uploadcolumn 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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