diff --git a/lib/koala/uploadable_io.rb b/lib/koala/uploadable_io.rb index e05eb62d..f90dce06 100644 --- a/lib/koala/uploadable_io.rb +++ b/lib/koala/uploadable_io.rb @@ -2,10 +2,13 @@ module Koala class UploadableIO + attr_reader :io_or_path, :content_type + def initialize(io_or_path_or_mixed, content_type = nil) + # see if we got the right inputs if content_type.nil? parse_init_mixed_param io_or_path_or_mixed - elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) || io_or_path_or_mixed.kind_of?(String)) + elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) or io_or_path_or_mixed.kind_of?(String)) @io_or_path = io_or_path_or_mixed @content_type = content_type else @@ -20,7 +23,9 @@ def to_upload_io private PARSE_STRATEGIES = [ :parse_rails_3_param, - :parse_sinatra_param + :parse_sinatra_param, + :parse_file_object, + :parse_string_path ] def parse_init_mixed_param(mixed) @@ -32,7 +37,7 @@ def parse_init_mixed_param(mixed) # Expects a parameter of type ActionDispatch::Http::UploadedFile def parse_rails_3_param(uploaded_file) - if uploaded_file.respond_to?(:content_type) && uploaded_file.respond_to?(:tempfile) && uploaded_file.tempfile.respond_to?(:path) + if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path) @io_or_path = uploaded_file.tempfile.path @content_type = uploaded_file.content_type end @@ -40,9 +45,65 @@ def parse_rails_3_param(uploaded_file) # Expects a Sinatra hash of file info def parse_sinatra_param(file_hash) - if file_hash.kind_of?(Hash) && file_hash.has_key?(:type) && file_hash.has_key?(:tempfile) + if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile) @io_or_path = file_hash[:tempfile] - @content_type = file_hash[:type] + @content_type = file_hash[:type] || detect_mime_type(tempfile) + end + end + + # takes a file object + def parse_file_object(file) + if file.kind_of?(File) + @io_or_path = file + @content_type = detect_mime_type(file.path) + end + end + + def parse_string_path(path) + if path.kind_of?(String) + @io_or_path = path + @content_type = detect_mime_type(path) + end + end + + MIME_TYPE_STRATEGIES = [ + :use_mime_module, + :use_simple_detection + ] + + def detect_mime_type(filename) + if filename + MIME_TYPE_STRATEGIES.each do |method| + result = send(method, filename) + return result if result + end + end + raise KoalaError, "UploadableIO unable to determine MIME type for #{filename}" + end + + def use_mime_module(filename) + # if the user has installed mime/types, we can use that + # if not, rescue and return nil + begin + type = MIME::Types.type_for(filename).first + type ? type.to_s : nil + rescue + nil + end + end + + def use_simple_detection(filename) + # very rudimentary extension analysis for images + # first, get the downcased extension, or an empty string if it doesn't exist + extension = ((filename.match(/\.([a-zA-Z0-9]+)/) || [])[1] || "").downcase + if extension == "" + nil + elsif extension == "jpg" || extension == "jpeg" + "image/jpeg" + elsif extension == "png" + "image/png" + elsif extension == "gif" + "image/gif" end end end diff --git a/spec/koala/uploadable_io/uploadable_io_tests.rb b/spec/koala/uploadable_io/uploadable_io_tests.rb index d403febe..333eaa31 100644 --- a/spec/koala/uploadable_io/uploadable_io_tests.rb +++ b/spec/koala/uploadable_io/uploadable_io_tests.rb @@ -1,3 +1,13 @@ +# fake MIME::Types +module Koala::MIME + module Types + def self.type_for(type) + # this should be faked out in tests + nil + end + end +end + class UploadableIOTests < Test::Unit::TestCase include Koala @@ -31,6 +41,24 @@ class UploadableIOTests < Test::Unit::TestCase lambda { UploadableIO.new({}) }.should_not raise_exception(Exception) lambda { UploadableIO.new({}) }.should_not raise_exception(KoalaError) end + + describe "for files with with recognizable MIME types" do + # what that means is tested below + + it "should accept a file object alone" do + params = [ + File.join(File.dirname(__FILE__), "..", "assets", "beach.jpg") + ] + lambda { UploadableIO.new(*params) }.should_not raise_exception(KoalaError) + end + + it "should accept a file path alone" do + params = [ + File.join(File.dirname(__FILE__), "..", "assets", "beach.jpg") + ] + lambda { UploadableIO.new(*params) }.should_not raise_exception(KoalaError) + end + end end describe "getting an UploadIO" do @@ -93,6 +121,64 @@ class UploadableIOTests < Test::Unit::TestCase end end + describe "when not given a content type" do + shared_examples_for "UploadableIO determining a content type" do + describe "if MIME::Types is available" do + it "should return an UploadIO with MIME::Types-determined type if the type exists" do + type_result = ["type"] + Koala::MIME::Types.stub(:type_for).and_return(type_result) + UploadableIO.new("myfilename.txt").content_type.should == type_result.first + end + end + + shared_examples_for "MIME::Types can't return results" do + { + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif" + }.each_pair do |extension, mime_type| + it "should properly get content types for #{extension} using basic analysis" do + UploadableIO.new("filename.#{extension}").content_type.should == mime_type + end + end + + it "should throw an exception if the MIME type can't be determined" do + lambda { UploadableIO.new("badfile.badextension") }.should raise_exception(KoalaError) + end + end + + describe "if MIME::Types is unavailable" do + before :each do + # fake that MIME::Types doesn't exist + Koala::MIME::Types.stub(:type_for).and_raise(NameError) + end + it_should_behave_like "MIME::Types can't return results" + end + + describe "if MIME::Types can't find the result" do + before :each do + # fake that MIME::Types doesn't exist + Koala::MIME::Types.stub(:type_for).and_return([]) + end + + it_should_behave_like "MIME::Types can't return results" + end + end # shared example group + + describe "for paths" do + before :each do + @koala_io_params = [ + "filename.abcd" + ] + end + + it_should_behave_like "UploadableIO determining a content type" + + end + + end + describe "when given a Rails 3 ActionDispatch::Http::UploadedFile" do before(:each) do @tempfile = stub('Tempfile', :path => true)