uploadcolumn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ module UploadColumn
2
+ module MagicColumns
3
+
4
+ def self.included(base)
5
+ super
6
+ base.send :alias_method_chain, :set_upload_column, :magic_columns
7
+ base.send :alias_method_chain, :set_upload_column_temp, :magic_columns
8
+ base.send :alias_method_chain, :save_uploaded_files, :magic_columns
9
+ end
10
+
11
+ def set_upload_column_with_magic_columns(name, file)
12
+ set_upload_column_without_magic_columns(name, file)
13
+ evaluate_magic_columns_for_upload_column(name)
14
+ end
15
+
16
+ def set_upload_column_temp_with_magic_columns(name, path)
17
+ set_upload_column_temp_without_magic_columns(name, path)
18
+ evaluate_magic_columns_for_upload_column(name)
19
+ end
20
+
21
+ def save_uploaded_files_with_magic_columns
22
+ save_uploaded_files_without_magic_columns
23
+ self.class.reflect_on_upload_columns.each do |name, column|
24
+ evaluate_magic_columns_for_upload_column(name)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def evaluate_magic_columns_for_upload_column(name)
31
+
32
+ self.class.column_names.each do |column_name|
33
+
34
+ statement, predicate = column_name.split('_', 2)
35
+
36
+ if statement and predicate and name.to_s == statement and not self.read_attribute(column_name.to_sym)
37
+ uploaded_file = self.send(:get_upload_column, name.to_sym)
38
+
39
+ self.write_attribute(column_name.to_sym, handle_predicate(uploaded_file, predicate))
40
+ end
41
+
42
+ end
43
+ end
44
+
45
+ def handle_predicate(uploaded_file, predicate)
46
+ return uploaded_file.send(predicate.to_sym) if uploaded_file.respond_to?(predicate.to_sym)
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,86 @@
1
+ module UploadColumn
2
+ module Manipulators
3
+
4
+ module ImageScience
5
+
6
+ attr_reader :width, :height
7
+
8
+ def load_manipulator_dependencies #:nodoc:
9
+ require 'image_science'
10
+ end
11
+
12
+ def process!(instruction)
13
+ if instruction.to_s =~ /^c(\d+x\d+)/
14
+ crop_resized!($1)
15
+ elsif instruction.to_s =~ /\d+x\d+/
16
+ resize!(instruction)
17
+ end
18
+ end
19
+
20
+ # Resize the image so that it will not exceed the dimensions passed
21
+ # via geometry, geometry should be a string, formatted like '200x100' where
22
+ # the first number is the height and the second is the width
23
+ def resize!( geometry )
24
+ ::ImageScience.with_image(self.path) do |img|
25
+ width, height = extract_dimensions(img.width, img.height, geometry)
26
+ img.resize( width, height ) do |file|
27
+ file.save( self.path )
28
+ end
29
+ end
30
+ end
31
+
32
+ # Resize and crop the image so that it will have the exact dimensions passed
33
+ # via geometry, geometry should be a string, formatted like '200x100' where
34
+ # the first number is the height and the second is the width
35
+ def crop_resized!( geometry )
36
+ ::ImageScience.with_image(self.path) do |img|
37
+ new_width, new_height = geometry.split('x').map{|i| i.to_i }
38
+
39
+ width, height = extract_dimensions_for_crop(img.width, img.height, geometry)
40
+ x_offset, y_offset = extract_placement_for_crop(width, height, geometry)
41
+
42
+ img.resize( width, height ) do |i2|
43
+
44
+ i2.with_crop( x_offset, y_offset, new_width + x_offset, new_height + y_offset) do |file|
45
+ file.save( self.path )
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def extract_dimensions(width, height, new_geometry, type = :resize)
54
+ new_width, new_height = convert_geometry(new_geometry)
55
+
56
+ aspect_ratio = width.to_f / height.to_f
57
+ new_aspect_ratio = new_width / new_height
58
+
59
+ if (new_aspect_ratio > aspect_ratio) ^ ( type == :crop ) # Image is too wide, the caret is the XOR operator
60
+ new_width, new_height = [ (new_height * aspect_ratio), new_height]
61
+ else #Image is too narrow
62
+ new_width, new_height = [ new_width, (new_width / aspect_ratio)]
63
+ end
64
+
65
+ [new_width, new_height].collect! { |v| v.round }
66
+ end
67
+
68
+ def extract_dimensions_for_crop(width, height, new_geometry)
69
+ extract_dimensions(width, height, new_geometry, :crop)
70
+ end
71
+
72
+ def extract_placement_for_crop(width, height, new_geometry)
73
+ new_width, new_height = convert_geometry(new_geometry)
74
+ x_offset = (width / 2.0) - (new_width / 2.0)
75
+ y_offset = (height / 2.0) - (new_height / 2.0)
76
+ [x_offset, y_offset].collect! { |v| v.round }
77
+ end
78
+
79
+ def convert_geometry(geometry)
80
+ geometry.split('x').map{|i| i.to_f }
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,75 @@
1
+ module UploadColumn
2
+
3
+ UploadError = Class.new(StandardError) unless defined?(UploadError)
4
+ ManipulationError = Class.new(UploadError) unless defined?(ManipulationError)
5
+
6
+ module Manipulators
7
+
8
+ module RMagick
9
+
10
+ def load_manipulator_dependencies #:nodoc:
11
+ require 'RMagick'
12
+ end
13
+
14
+ def process!(instruction = nil, &block)
15
+ if instruction.is_a?(Proc)
16
+ manipulate!(&instruction)
17
+ elsif instruction.to_s =~ /^c(\d+x\d+)$/
18
+ crop_resized!($1)
19
+ elsif instruction.to_s =~ /^(\d+x\d+)$/
20
+ resize!($1)
21
+ end
22
+ manipulate!(&block) if block
23
+ end
24
+
25
+ # Convert the image to format
26
+ def convert!(format)
27
+ manipulate! do |img|
28
+ img.format = format.to_s.upcase
29
+ img
30
+ end
31
+ end
32
+
33
+ # Resize the image so that it will not exceed the dimensions passed
34
+ # via geometry, geometry should be a string, formatted like '200x100' where
35
+ # the first number is the height and the second is the width
36
+ def resize!( geometry )
37
+ manipulate! do |img|
38
+ img.change_geometry( geometry ) do |c, r, i|
39
+ i.resize(c,r)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Resize and crop the image so that it will have the exact dimensions passed
45
+ # via geometry, geometry should be a string, formatted like '200x100' where
46
+ # the first number is the height and the second is the width
47
+ def crop_resized!( geometry )
48
+ manipulate! do |img|
49
+ h, w = geometry.split('x')
50
+ img.crop_resized(h.to_i,w.to_i)
51
+ end
52
+ end
53
+
54
+ def manipulate!
55
+ image = ::Magick::Image.read(self.path)
56
+
57
+ if image.size > 1
58
+ list = ::Magick::ImageList.new
59
+ image.each do |frame|
60
+ list << yield( frame )
61
+ end
62
+ list.write(self.path)
63
+ else
64
+ yield( image.first ).write(self.path)
65
+ end
66
+ rescue ::Magick::ImageMagickError => e
67
+ # this is a more meaningful error message, which we could catch later
68
+ raise ManipulationError.new("Failed to manipulate with rmagick, maybe it is not an image? Original Error: #{e}")
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,61 @@
1
+ module UploadColumn::ActionControllerExtension
2
+
3
+ def self.included(base)
4
+ base.send :alias_method_chain, :url_for, :uploaded_file_check
5
+ base.helper_method :url_for_path
6
+ end
7
+
8
+ protected
9
+
10
+ def url_for_with_uploaded_file_check(options = {}, *parameters_for_method_reference)
11
+ if(options.respond_to?(:public_path))
12
+ options.public_path
13
+ else
14
+ url_for_without_uploaded_file_check(options || {}, *parameters_for_method_reference)
15
+ end
16
+ end
17
+
18
+ def url_for_path(path)
19
+ request.protocol + request.host_with_port + path
20
+ end
21
+
22
+ # You can use +render_image+ in your controllers to render an image
23
+ # def picture
24
+ # @user = User.find(params[:id])
25
+ # render_image @user.picture
26
+ # end
27
+ # This of course, is not very useful at all (you could simply have linked to the image itself),
28
+ # However it is even possible to pass a block to render_image that allows manipulation using
29
+ # RMagick, here the fun begins:
30
+ # def solarize_picture
31
+ # @user = User.find(params[:id])
32
+ # render_image @user.picture do |img|
33
+ # img = img.segment
34
+ # img.solarize
35
+ # end
36
+ # end
37
+ # Note that like in UploadColumn::BaseUploadedFile.process you will need to 'carry' the image
38
+ # since most Rmagick methods do not modify the image itself but rather return the result of the
39
+ # transformation.
40
+ #
41
+ # Instead of passing an upload_column object to +render_image+ you can even pass a path String,
42
+ # if you do you will have to pass a :mime-type option as well though.
43
+ def render_image( file, options = {} )
44
+ format = if options.is_a?(Hash) then options[:force_format] else nil end
45
+ mime_type = if options.is_a?(String) then options else options[:mime_type] end
46
+ mime_type ||= file.mime_type
47
+ path = if file.is_a?( String ) then file else file.path end
48
+ headers["Content-Type"] = mime_type unless format
49
+
50
+ if block_given? or format
51
+ img = ::Magick::Image::read(path).first
52
+ img = yield( img ) if block_given?
53
+ img.format = format.to_s.upcase if format
54
+ render :text => img.to_blob, :layout => false
55
+ else
56
+ send_file( path )
57
+ end
58
+ end
59
+ end
60
+
61
+ ActionController::Base.send(:include, UploadColumn::ActionControllerExtension)
@@ -0,0 +1,17 @@
1
+ module UploadColumn::AssetTagExtension
2
+
3
+ def self.included(base)
4
+ base.send :alias_method_chain, :image_tag, :uploaded_file_check
5
+ end
6
+
7
+ def image_tag_with_uploaded_file_check(source, options = {})
8
+ if(source.respond_to?(:public_path))
9
+ image_tag_without_uploaded_file_check(source.public_path, options)
10
+ else
11
+ image_tag_without_uploaded_file_check(source, options)
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ ActionView::Helpers::AssetTagHelper.send(:include, UploadColumn::AssetTagExtension)
@@ -0,0 +1,45 @@
1
+ module UploadColumn::UploadColumnHelper
2
+
3
+ # Returns an input tag of the "file" type tailored for accessing an upload_column field
4
+ # (identified by method) on an object assigned to the template (identified by object).
5
+ # Additional options on the input tag can be passed as a hash with options.
6
+ #
7
+ # Example (call, result)
8
+ # upload_column_field( :user, :picture )
9
+ # <input id="user_picture_temp" name="user[picture_temp]" type="hidden" />
10
+ # <input id="user_picture" name="user[picture]" size="30" type="file" />
11
+ #
12
+ # Note: if you use file_field instead of upload_column_field, the file will not be
13
+ # stored across form redisplays.
14
+ def upload_column_field(object, method, options={})
15
+ file_field(object, method, options) + hidden_field(object, method.to_s + '_temp')
16
+ end
17
+
18
+ # A helper method for creating a form tag to use with uploadng files,
19
+ # it works exactly like Rails' form_tag, except that :multipart is always true
20
+ def upload_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc)
21
+ options[:multipart] = true
22
+ form_tag( url_for_options, options, *parameters_for_url, &proc )
23
+ end
24
+
25
+ # A helper method for creating a form tag to use with uploadng files,
26
+ # it works exactly like Rails' form_for, except that :multipart is always true
27
+ def upload_form_for(*args, &block)
28
+ options = args.extract_options!
29
+ options[:html] ||= {}
30
+ options[:html][:multipart] = true
31
+ args.push(options)
32
+
33
+ form_for(*args, &block)
34
+ end
35
+
36
+ end
37
+
38
+ class ActionView::Helpers::FormBuilder #:nodoc:
39
+ self.field_helpers += ['upload_column_field']
40
+ def upload_column_field(method, options = {})
41
+ @template.send(:upload_column_field, @object_name, method, options.merge(:object => @object))
42
+ end
43
+ end
44
+
45
+ ActionView::Base.send(:include, UploadColumn::UploadColumnHelper)
@@ -0,0 +1,176 @@
1
+ begin; require 'mime/types'; rescue Exception; end
2
+
3
+ require 'fileutils'
4
+
5
+ module UploadColumn
6
+ # Sanitize is a base class that takes care of all the dirtywork when dealing with file uploads.
7
+ # it is subclassed as UploadedFile in UploadColumn, which does most of the upload magic, but if
8
+ # you want to roll you own uploading system, SanitizedFile might be for you since it takes care
9
+ # of a lot of the unfun stuff.
10
+ #
11
+ # Usage is pretty simple, just do SanitizedFile.new(some_uploaded_file) and you're good to go
12
+ # you can now use #copy_to and #move_to to place the file wherever you want, whether it is a StringIO
13
+ # or a TempFile.
14
+ #
15
+ # SanitizedFile also deals with content type detection, which it does either through the 'file' *nix exec
16
+ # or (if you are stuck on Windows) through the MIME::Types library (not to be confused with Rails' Mime class!).
17
+ class SanitizedFile
18
+
19
+ attr_reader :basename, :extension
20
+
21
+ def initialize(file, options = {})
22
+ @options = options
23
+ if file && file.instance_of?(String) && !file.empty?
24
+ @path = file
25
+ self.filename = File.basename(file)
26
+ else
27
+ @file = file
28
+ self.filename = self.original_filename unless self.empty?
29
+ end
30
+ end
31
+
32
+ # Returns the filename before sanitation took place
33
+ def original_filename
34
+ @original_filename ||= if @file and @file.respond_to?(:original_filename)
35
+ @file.original_filename
36
+ elsif self.path
37
+ File.basename(self.path)
38
+ end
39
+ end
40
+
41
+ # Returns the files properly sanitized filename.
42
+ def filename
43
+ @filename ||= (self.extension && !self.extension.empty?) ? "#{self.basename}.#{self.extension}" : self.basename
44
+ end
45
+
46
+ # Returns the file's size
47
+ def size
48
+ return @file.size if @file.respond_to?(:size)
49
+ File.size(self.path) rescue nil
50
+ end
51
+
52
+ # Returns the full path to the file
53
+ def path
54
+ @path ||= File.expand_path(@file.path) rescue nil
55
+ end
56
+
57
+ # Checks if the file is empty.
58
+ def empty?
59
+ (@file.nil? && @path.nil?) || self.size.nil? || self.size.zero?
60
+ end
61
+
62
+ # Checks if the file exists
63
+ def exists?
64
+ File.exists?(self.path) if self.path
65
+ end
66
+
67
+ # Moves the file to 'path'
68
+ def move_to(path)
69
+ if copy_file(path)
70
+ # FIXME: This gets pretty broken in UploadedFile. E.g. moving avatar-thumb.jpg will change the filename
71
+ # to avatar-thumb-thumb.jpg
72
+ @basename, @extension = split_extension(File.basename(path))
73
+ @file = nil
74
+ @filename = nil
75
+ @path = path
76
+ end
77
+ end
78
+
79
+ # Copies the file to 'path' and returns a new SanitizedFile that points to the copy.
80
+ def copy_to(path)
81
+ copy = self.clone
82
+ copy.move_to(path)
83
+ return copy
84
+ end
85
+
86
+ # Returns the content_type of the file as determined through the MIME::Types library or through a *nix exec.
87
+ def content_type
88
+ unless content_type = get_content_type_from_exec || get_content_type_from_mime_types
89
+ content_type ||= @file.content_type.chomp if @file.respond_to?(:content_type) and @file.content_type
90
+ end
91
+ return content_type
92
+ end
93
+
94
+ private
95
+
96
+ def copy_file(path)
97
+ unless self.empty?
98
+ # create the directory if it doesn't exist
99
+ FileUtils.mkdir_p(File.dirname(path)) unless File.exists?(File.dirname(path))
100
+ # stringios don't have a path and can't be copied
101
+ if not self.path and @file.respond_to?(:read)
102
+ @file.rewind # Make sure we are at the beginning of the buffer
103
+ File.open(path, "wb") { |f| f.write(@file.read) }
104
+ else
105
+ begin
106
+ FileUtils.cp(self.path, path)
107
+ rescue ArgumentError
108
+ end
109
+ end
110
+ File.chmod(@options[:permissions], path) if @options[:permissions]
111
+ return true
112
+ end
113
+ end
114
+
115
+ def filename=(filename)
116
+ basename, extension = split_extension(filename)
117
+ @basename = sanitize(basename)
118
+ @extension = correct_file_extension(extension)
119
+ end
120
+
121
+ # tries to identify the mime-type of file and correct self's extension
122
+ # based on the found mime-type
123
+ def correct_file_extension(ext)
124
+ if @options[:fix_file_extensions] && defined?(MIME::Types)
125
+ if mimes = MIME::Types[self.content_type]
126
+ return mimes.first.extensions.first unless mimes.first.extensions.empty?
127
+ end
128
+ end
129
+ return ext.downcase
130
+ end
131
+
132
+ # Try to use *nix exec to fetch content type
133
+ def get_content_type_from_exec
134
+ if @options[:get_content_type_from_file_exec] and not self.path.empty?
135
+ return system_call(%(file -bi "#{self.path}")).chomp.scan(/^[a-z0-9\-_]+\/[a-z0-9\-_]+/).first
136
+ end
137
+ rescue
138
+ nil
139
+ end
140
+
141
+ def system_call(command)
142
+ `#{command}`
143
+ end
144
+
145
+ def get_content_type_from_mime_types
146
+ if @extension and defined?(MIME::Types)
147
+ mimes = MIME::Types.of(@extension)
148
+ return mimes.first.content_type rescue nil
149
+ end
150
+ end
151
+
152
+ def sanitize(name)
153
+ # Sanitize the filename, to prevent hacking
154
+ name = File.basename(name.gsub("\\", "/")) # work-around for IE
155
+ name.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
156
+ name = "_#{name}" if name =~ /^\.+$/
157
+ name = "unnamed" if name.size == 0
158
+ return name.downcase
159
+ end
160
+
161
+ def split_extension(fn)
162
+ # regular expressions to try for identifying extensions
163
+ ext_regexps = [
164
+ /^(.+)\.([^\.]{1,3}\.[^\.]{1,4})$/, # matches "something.tar.gz"
165
+ /^(.+)\.([^\.]+)$/ # matches "something.jpg"
166
+ ]
167
+ ext_regexps.each do |regexp|
168
+ if fn =~ regexp
169
+ return $1, $2
170
+ end
171
+ end
172
+ return fn, "" # In case we weren't able to split the extension
173
+ end
174
+
175
+ end
176
+ end