carrierwave 0.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.
- data/Generators +4 -0
- data/LICENSE +20 -0
- data/README.md +242 -0
- data/Rakefile +96 -0
- data/TODO +0 -0
- data/lib/carrierwave.rb +67 -0
- data/lib/carrierwave/mount.rb +153 -0
- data/lib/carrierwave/orm/activerecord.rb +20 -0
- data/lib/carrierwave/orm/datamapper.rb +20 -0
- data/lib/carrierwave/processing/image_science.rb +70 -0
- data/lib/carrierwave/processing/rmagick.rb +161 -0
- data/lib/carrierwave/sanitized_file.rb +242 -0
- data/lib/carrierwave/storage/abstract.rb +80 -0
- data/lib/carrierwave/storage/file.rb +40 -0
- data/lib/carrierwave/storage/s3.rb +83 -0
- data/lib/carrierwave/uploader.rb +427 -0
- data/lib/generators/uploader_generator.rb +20 -0
- data/rails_generators/uploader/USAGE +2 -0
- data/rails_generators/uploader/templates/uploader.rb +33 -0
- data/rails_generators/uploader/uploader_generator.rb +19 -0
- data/spec/fixtures/bork.txt +1 -0
- data/spec/fixtures/test.jpeg +1 -0
- data/spec/fixtures/test.jpg +1 -0
- data/spec/mount_spec.rb +181 -0
- data/spec/orm/activerecord_spec.rb +168 -0
- data/spec/orm/datamapper_spec.rb +133 -0
- data/spec/sanitized_file_spec.rb +624 -0
- data/spec/spec_helper.rb +120 -0
- data/spec/uploader_spec.rb +739 -0
- metadata +92 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'activerecord'
|
2
|
+
|
3
|
+
module CarrierWave
|
4
|
+
module ActiveRecord
|
5
|
+
|
6
|
+
include CarrierWave::Mount
|
7
|
+
|
8
|
+
def after_mount(column, uploader)
|
9
|
+
alias_method :read_uploader, :read_attribute
|
10
|
+
alias_method :write_uploader, :write_attribute
|
11
|
+
|
12
|
+
before_save do |record|
|
13
|
+
record.send("store_#{column}!")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end # ActiveRecord
|
18
|
+
end # CarrierWave
|
19
|
+
|
20
|
+
ActiveRecord::Base.send(:extend, CarrierWave::ActiveRecord)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
|
3
|
+
module CarrierWave
|
4
|
+
module DataMapper
|
5
|
+
|
6
|
+
include CarrierWave::Mount
|
7
|
+
|
8
|
+
def after_mount(column, uploader)
|
9
|
+
alias_method :read_uploader, :attribute_get
|
10
|
+
alias_method :write_uploader, :attribute_set
|
11
|
+
|
12
|
+
before :save do
|
13
|
+
send("store_#{column}!")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end # DataMapper
|
18
|
+
end # CarrierWave
|
19
|
+
|
20
|
+
DataMapper::Model.send(:include, CarrierWave::DataMapper)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "image_science"
|
2
|
+
|
3
|
+
module CarrierWave
|
4
|
+
module ImageScience
|
5
|
+
|
6
|
+
# Resize the image so that it will not exceed the dimensions passed
|
7
|
+
# via geometry, geometry should be a string, formatted like '200x100' where
|
8
|
+
# the first number is the height and the second is the width
|
9
|
+
def resize!( geometry )
|
10
|
+
::ImageScience.with_image(self.current_path) do |img|
|
11
|
+
width, height = extract_dimensions(img.width, img.height, geometry)
|
12
|
+
img.resize( width, height ) do |file|
|
13
|
+
file.save( self.current_path )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Resize and crop the image so that it will have the exact dimensions passed
|
19
|
+
# via geometry, geometry should be a string, formatted like '200x100' where
|
20
|
+
# the first number is the height and the second is the width
|
21
|
+
def crop_resized!( geometry )
|
22
|
+
::ImageScience.with_image(self.current_path) do |img|
|
23
|
+
new_width, new_height = geometry.split('x').map{|i| i.to_i }
|
24
|
+
|
25
|
+
width, height = extract_dimensions_for_crop(img.width, img.height, geometry)
|
26
|
+
x_offset, y_offset = extract_placement_for_crop(width, height, geometry)
|
27
|
+
|
28
|
+
img.resize( width, height ) do |i2|
|
29
|
+
|
30
|
+
i2.with_crop( x_offset, y_offset, new_width + x_offset, new_height + y_offset) do |file|
|
31
|
+
file.save( self.current_path )
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def extract_dimensions(width, height, new_geometry, type = :resize)
|
40
|
+
new_width, new_height = convert_geometry(new_geometry)
|
41
|
+
|
42
|
+
aspect_ratio = width.to_f / height.to_f
|
43
|
+
new_aspect_ratio = new_width / new_height
|
44
|
+
|
45
|
+
if (new_aspect_ratio > aspect_ratio) ^ ( type == :crop ) # Image is too wide, the caret is the XOR operator
|
46
|
+
new_width, new_height = [ (new_height * aspect_ratio), new_height]
|
47
|
+
else #Image is too narrow
|
48
|
+
new_width, new_height = [ new_width, (new_width / aspect_ratio)]
|
49
|
+
end
|
50
|
+
|
51
|
+
[new_width, new_height].collect! { |v| v.round }
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_dimensions_for_crop(width, height, new_geometry)
|
55
|
+
extract_dimensions(width, height, new_geometry, :crop)
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_placement_for_crop(width, height, new_geometry)
|
59
|
+
new_width, new_height = convert_geometry(new_geometry)
|
60
|
+
x_offset = (width / 2.0) - (new_width / 2.0)
|
61
|
+
y_offset = (height / 2.0) - (new_height / 2.0)
|
62
|
+
[x_offset, y_offset].collect! { |v| v.round }
|
63
|
+
end
|
64
|
+
|
65
|
+
def convert_geometry(geometry)
|
66
|
+
geometry.split('x').map{|i| i.to_f }
|
67
|
+
end
|
68
|
+
|
69
|
+
end # ImageScience
|
70
|
+
end # CarrierWave
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'rmagick'
|
2
|
+
|
3
|
+
module CarrierWave
|
4
|
+
|
5
|
+
##
|
6
|
+
# This module simplifies manipulation with RMagick by providing a set
|
7
|
+
# of convenient helper methods. If you want to use them, you'll need to
|
8
|
+
# require this file
|
9
|
+
#
|
10
|
+
# require 'carrierwave/processing/rmagick'
|
11
|
+
#
|
12
|
+
# And then include it in your uploader
|
13
|
+
#
|
14
|
+
# MyUploade < CarrierWave::Uploader
|
15
|
+
# include CarrierWave::RMagick
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# You can now use the provided helpers:
|
19
|
+
#
|
20
|
+
# MyUploade < CarrierWave::Uploader
|
21
|
+
# include CarrierWave::RMagick
|
22
|
+
#
|
23
|
+
# process :resize_to_fit => [200, 200]
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Or create your own helpers with the powerful manipulate! method. Check
|
27
|
+
# out the RMagick docs at http://www.imagemagick.org/RMagick/doc/ for more
|
28
|
+
# info
|
29
|
+
#
|
30
|
+
# MyUploade < CarrierWave::Uploader
|
31
|
+
# include CarrierWave::RMagick
|
32
|
+
#
|
33
|
+
# process :do_stuff => 10.0
|
34
|
+
#
|
35
|
+
# def do_stuff(blur_factor)
|
36
|
+
# manipulate! do |img|
|
37
|
+
# img = img.sepiatone
|
38
|
+
# img = img.auto_orient
|
39
|
+
# img = img.radial_blur(blur_factor)
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
module RMagick
|
45
|
+
|
46
|
+
##
|
47
|
+
# Changes the image encoding format to the given format
|
48
|
+
#
|
49
|
+
# @see http://www.imagemagick.org/RMagick/doc/magick.html#formats
|
50
|
+
# @param [#to_s] format an abreviation of the format
|
51
|
+
# @yieldparam [Magick::Image] img additional manipulations to perform
|
52
|
+
# @example
|
53
|
+
# image.convert(:png)
|
54
|
+
#
|
55
|
+
def convert(format)
|
56
|
+
manipulate! do |img|
|
57
|
+
img.format = format.to_s.upcase
|
58
|
+
img = yield(img) if block_given?
|
59
|
+
img
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# From the RMagick documentation: "Resize the image to fit within the
|
65
|
+
# specified dimensions while retaining the original aspect ratio. The
|
66
|
+
# image may be shorter or narrower than specified in the smaller dimension
|
67
|
+
# but will not be larger than the specified values."
|
68
|
+
#
|
69
|
+
# @see http://www.imagemagick.org/RMagick/doc/image3.html#resize_to_fit
|
70
|
+
#
|
71
|
+
# @param [Integer] width the width to scale the image to
|
72
|
+
# @param [Integer] height the height to scale the image to
|
73
|
+
# @yieldparam [Magick::Image] img additional manipulations to perform
|
74
|
+
#
|
75
|
+
def resize_to_fit(width, height)
|
76
|
+
manipulate! do |img|
|
77
|
+
img.resize_to_fit!(width, height)
|
78
|
+
img = yield(img) if block_given?
|
79
|
+
img
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
alias_method :resize, :resize_to_fit
|
84
|
+
|
85
|
+
##
|
86
|
+
# From the RMagick documentation: "Resize the image to fit within the
|
87
|
+
# specified dimensions while retaining the aspect ratio of the original
|
88
|
+
# image. If necessary, crop the image in the larger dimension."
|
89
|
+
#
|
90
|
+
# @see http://www.imagemagick.org/RMagick/doc/image3.html#resize_to_fill
|
91
|
+
#
|
92
|
+
# @param [Integer] width the width to scale the image to
|
93
|
+
# @param [Integer] height the height to scale the image to
|
94
|
+
# @yieldparam [Magick::Image] img additional manipulations to perform
|
95
|
+
#
|
96
|
+
def resize_to_fill(width, height)
|
97
|
+
manipulate! do |img|
|
98
|
+
img.resize_to_fill!(width, height)
|
99
|
+
img = yield(img) if block_given?
|
100
|
+
img
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
alias_method :crop_resized, :resize_to_fill
|
105
|
+
|
106
|
+
##
|
107
|
+
# Resize the image to fit within the specified dimensions while retaining
|
108
|
+
# the original aspect ratio. If necessary will pad the remaining area
|
109
|
+
# with the given color, which defaults to transparent (for gif and png,
|
110
|
+
# white for jpeg).
|
111
|
+
#
|
112
|
+
# @param [Integer] width the width to scale the image to
|
113
|
+
# @param [Integer] height the height to scale the image to
|
114
|
+
# @param [String, :transparent] background the color of the background as a hexcode, like "#ff45de"
|
115
|
+
# @param [Magick::GravityType] gravity how to position the image
|
116
|
+
# @yieldparam [Magick::Image] img additional manipulations to perform
|
117
|
+
#
|
118
|
+
def resize_and_pad(width, height, background=:transparent, gravity=::Magick::CenterGravity)
|
119
|
+
manipulate! do |img|
|
120
|
+
img.resize_to_fit!(width, height)
|
121
|
+
new_img = ::Magick::Image.new(width, height)
|
122
|
+
if background == :transparent
|
123
|
+
new_img = new_img.matte_floodfill(1, 1)
|
124
|
+
else
|
125
|
+
new_img = new_img.color_floodfill(1, 1, ::Magick::Pixel.from_color(background))
|
126
|
+
end
|
127
|
+
new_img = new_img.composite(img, gravity, ::Magick::OverCompositeOp)
|
128
|
+
new_img = yield(new_img) if block_given?
|
129
|
+
new_img
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Manipulate the image with RMagick. This method will load up an image
|
135
|
+
# and then pass each of its frames to the supplied block. It will then
|
136
|
+
# save the image to disk.
|
137
|
+
#
|
138
|
+
# Note: This method assumes that the object responds to current_path
|
139
|
+
# any class that this is mixed into must have a current_path method.
|
140
|
+
#
|
141
|
+
# @yieldparam [Magick::Image] img manipulations to perform
|
142
|
+
# @raise [CarrierWave::ProcessingError] if manipulation failed.
|
143
|
+
#
|
144
|
+
def manipulate!
|
145
|
+
image = ::Magick::Image.read(current_path)
|
146
|
+
|
147
|
+
if image.size > 1
|
148
|
+
list = ::Magick::ImageList.new
|
149
|
+
image.each do |frame|
|
150
|
+
list << yield( frame )
|
151
|
+
end
|
152
|
+
list.write(current_path)
|
153
|
+
else
|
154
|
+
yield( image.first ).write(current_path)
|
155
|
+
end
|
156
|
+
rescue ::Magick::ImageMagickError => e
|
157
|
+
raise CarrierWave::ProcessingError.new("Failed to manipulate with rmagick, maybe it is not an image? Original Error: #{e}")
|
158
|
+
end
|
159
|
+
|
160
|
+
end # RMagick
|
161
|
+
end # CarrierWave
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module CarrierWave
|
2
|
+
|
3
|
+
##
|
4
|
+
# SanitizedFile is a base class which provides a common API around all
|
5
|
+
# the different quirky Ruby File libraries. It has support for Tempfile,
|
6
|
+
# File, StringIO, Merb-style upload Hashes, as well as paths given as
|
7
|
+
# Strings and Pathnames.
|
8
|
+
#
|
9
|
+
# It's probably needlessly comprehensive and complex. Help is appreciated.
|
10
|
+
#
|
11
|
+
class SanitizedFile
|
12
|
+
|
13
|
+
attr_accessor :file, :options
|
14
|
+
|
15
|
+
def initialize(file, options = {})
|
16
|
+
self.file = file
|
17
|
+
self.options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Returns the filename as is, without sanizting it.
|
22
|
+
#
|
23
|
+
# @return [String] the unsanitized filename
|
24
|
+
#
|
25
|
+
def original_filename
|
26
|
+
return @original_filename if @original_filename
|
27
|
+
if @file and @file.respond_to?(:original_filename)
|
28
|
+
@file.original_filename
|
29
|
+
elsif path
|
30
|
+
File.basename(path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Returns the filename, sanitized to strip out any evil characters.
|
36
|
+
#
|
37
|
+
# @return [String] the sanitized filename
|
38
|
+
#
|
39
|
+
def filename
|
40
|
+
sanitize(original_filename) if original_filename
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :identifier, :filename
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns the part of the filename before the extension. So if a file is called 'test.jpeg'
|
47
|
+
# this would return 'test'
|
48
|
+
#
|
49
|
+
# @return [String] the first part of the filename
|
50
|
+
#
|
51
|
+
def basename
|
52
|
+
split_extension(filename)[0] if filename
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Returns the file extension
|
57
|
+
#
|
58
|
+
# @return [String] the extension
|
59
|
+
#
|
60
|
+
def extension
|
61
|
+
split_extension(filename)[1] if filename
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns the file's size.
|
66
|
+
#
|
67
|
+
# @return [Integer] the file's size in bytes.
|
68
|
+
#
|
69
|
+
def size
|
70
|
+
if string?
|
71
|
+
exists? ? File.size(path) : 0
|
72
|
+
elsif @file.respond_to?(:size)
|
73
|
+
@file.size
|
74
|
+
elsif path
|
75
|
+
exists? ? File.size(path) : 0
|
76
|
+
else
|
77
|
+
0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Returns the full path to the file. If the file has no path, it will return nil.
|
83
|
+
#
|
84
|
+
# @return [String, nil] the path where the file is located.
|
85
|
+
#
|
86
|
+
def path
|
87
|
+
unless @file.blank?
|
88
|
+
if string?
|
89
|
+
File.expand_path(@file)
|
90
|
+
elsif @file.respond_to?(:path) and not @file.path.blank?
|
91
|
+
File.expand_path(@file.path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Returns true if the file is supplied as a pathname or as a string.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
#
|
101
|
+
def string?
|
102
|
+
!!((@file.is_a?(String) || @file.is_a?(Pathname)) && !@file.blank?)
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Checks if the file is valid and has a non-zero size
|
107
|
+
#
|
108
|
+
# @return [Boolean]
|
109
|
+
#
|
110
|
+
def empty?
|
111
|
+
@file.nil? || self.size.nil? || self.size.zero?
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Checks if the file exists
|
116
|
+
#
|
117
|
+
# @return [Boolean]
|
118
|
+
#
|
119
|
+
def exists?
|
120
|
+
return File.exists?(self.path) if self.path
|
121
|
+
return false
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Returns the contents of the file.
|
126
|
+
#
|
127
|
+
# @return [String] contents of the file
|
128
|
+
#
|
129
|
+
def read
|
130
|
+
if string?
|
131
|
+
File.read(@file)
|
132
|
+
else
|
133
|
+
@file.rewind if @file.respond_to?(:rewind)
|
134
|
+
@file.read
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Moves the file to the given path
|
140
|
+
#
|
141
|
+
# @param [String] new_path The path where the file should be moved.
|
142
|
+
#
|
143
|
+
def move_to(new_path)
|
144
|
+
return if self.empty?
|
145
|
+
new_path = File.expand_path(new_path)
|
146
|
+
|
147
|
+
mkdir!(new_path)
|
148
|
+
if exists?
|
149
|
+
FileUtils.mv(path, new_path) unless new_path == path
|
150
|
+
else
|
151
|
+
File.open(new_path, "wb") { |f| f.write(read) }
|
152
|
+
end
|
153
|
+
chmod!(new_path)
|
154
|
+
self.file = new_path
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Creates a copy of this file and moves it to the given path. Returns the copy.
|
159
|
+
#
|
160
|
+
# @param [String] new_path The path where the file should be copied to.
|
161
|
+
# @return [CarrierWave::SanitizedFile] the location where the file will be stored.
|
162
|
+
#
|
163
|
+
def copy_to(new_path)
|
164
|
+
return if self.empty?
|
165
|
+
new_path = File.expand_path(new_path)
|
166
|
+
|
167
|
+
mkdir!(new_path)
|
168
|
+
if exists?
|
169
|
+
FileUtils.cp(path, new_path) unless new_path == path
|
170
|
+
else
|
171
|
+
File.open(new_path, "wb") { |f| f.write(read) }
|
172
|
+
end
|
173
|
+
chmod!(new_path)
|
174
|
+
self.class.new(new_path)
|
175
|
+
end
|
176
|
+
|
177
|
+
##
|
178
|
+
# Removes the file from the filesystem.
|
179
|
+
#
|
180
|
+
def delete
|
181
|
+
FileUtils.rm(self.path) if exists?
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Returns the content type of the file.
|
186
|
+
#
|
187
|
+
# @return [String] the content type of the file
|
188
|
+
#
|
189
|
+
def content_type
|
190
|
+
return @content_type if @content_type
|
191
|
+
@file.content_type.chomp if @file.respond_to?(:content_type) and @file.content_type
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def file=(file)
|
197
|
+
if file.is_a?(Hash)
|
198
|
+
@file = file["tempfile"]
|
199
|
+
@original_filename = file["filename"]
|
200
|
+
@content_type = file["content_type"]
|
201
|
+
else
|
202
|
+
@file = file
|
203
|
+
@original_filename = nil
|
204
|
+
@content_type = nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# create the directory if it doesn't exist
|
209
|
+
def mkdir!(path)
|
210
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.exists?(File.dirname(path))
|
211
|
+
end
|
212
|
+
|
213
|
+
def chmod!(path)
|
214
|
+
File.chmod(@options[:permissions], path) if @options[:permissions]
|
215
|
+
end
|
216
|
+
|
217
|
+
# Sanitize the filename, to prevent hacking
|
218
|
+
def sanitize(name)
|
219
|
+
name = name.gsub("\\", "/") # work-around for IE
|
220
|
+
name = File.basename(name)
|
221
|
+
name = name.gsub(/[^a-zA-Z0-9\.\-\+_]/,"_")
|
222
|
+
name = "_#{name}" if name =~ /^\.+$/
|
223
|
+
name = "unnamed" if name.size == 0
|
224
|
+
return name.downcase
|
225
|
+
end
|
226
|
+
|
227
|
+
def split_extension(fn)
|
228
|
+
# regular expressions to try for identifying extensions
|
229
|
+
ext_regexps = [
|
230
|
+
/^(.+)\.([^\.]{1,3}\.[^\.]{1,4})$/, # matches "something.tar.gz"
|
231
|
+
/^(.+)\.([^\.]+)$/ # matches "something.jpg"
|
232
|
+
]
|
233
|
+
ext_regexps.each do |regexp|
|
234
|
+
if fn =~ regexp
|
235
|
+
return $1, $2
|
236
|
+
end
|
237
|
+
end
|
238
|
+
return fn, "" # In case we weren't able to split the extension
|
239
|
+
end
|
240
|
+
|
241
|
+
end # SanitizedFile
|
242
|
+
end # CarrierWave
|