From f619a5d334c22effb8210bae7b3db38d0f3ba929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohnic=CC=81?= Date: Sun, 24 Feb 2013 22:36:29 +0100 Subject: [PATCH] fix file uploads with Ruby 2.0 net/http Rewrite Faraday::CompositeReadIO because the one inherited from multipart-post library doesn't behave well when used with IO.copy_stream which net/http uses internally in Ruby 2.0. --- lib/faraday/upload_io.rb | 48 +++++++++++++-- test/composite_read_io_test.rb | 107 +++++++++++++++++++++++++++++++++ test/multibyte.txt | 1 + 3 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 test/composite_read_io_test.rb create mode 100644 test/multibyte.txt diff --git a/lib/faraday/upload_io.rb b/lib/faraday/upload_io.rb index b9acb5c5..2121385d 100644 --- a/lib/faraday/upload_io.rb +++ b/lib/faraday/upload_io.rb @@ -8,13 +8,49 @@ rescue LoadError end module Faraday - class CompositeReadIO < ::CompositeReadIO - attr_reader :length + # Similar but not compatible with ::CompositeReadIO provided by multipart-post. + class CompositeReadIO + def initialize(*parts) + @parts = parts.flatten + @ios = @parts.map { |part| part.to_io } + @index = 0 + end - def initialize(parts) - @length = parts.inject(0) { |sum, part| sum + part.length } - ios = parts.map{ |part| part.to_io } - super(*ios) + def length + @parts.inject(0) { |sum, part| sum + part.length } + end + + def rewind + @ios.each { |io| io.rewind } + @index = 0 + end + + # Read from IOs in order until `length` bytes have been received. + def read(length = nil, outbuf = nil) + got_result = false + outbuf = outbuf ? outbuf.replace("") : "" + + while io = current_io + if result = io.read(length) + got_result ||= !result.nil? + result.force_encoding("BINARY") if result.respond_to?(:force_encoding) + outbuf << result + length -= result.length if length + break if length == 0 + end + advance_io + end + (!got_result && length) ? nil : outbuf + end + + private + + def current_io + @ios[@index] + end + + def advance_io + @index += 1 end end diff --git a/test/composite_read_io_test.rb b/test/composite_read_io_test.rb new file mode 100644 index 00000000..be0af773 --- /dev/null +++ b/test/composite_read_io_test.rb @@ -0,0 +1,107 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'helper')) +require 'stringio' + +class CompositeReadIOTest < MiniTest::Unit::TestCase + Part = Struct.new(:to_io) do + def length() to_io.string.length end + end + + def part(str) + Part.new StringIO.new(str) + end + + def composite_io(*parts) + Faraday::CompositeReadIO.new(*parts) + end + + def test_empty + io = composite_io + assert_equal 0, io.length + assert_equal "", io.read + end + + def test_empty_returns_nil_for_limited_read + assert_nil composite_io.read(1) + end + + def test_empty_parts_returns_nil_for_limited_read + io = composite_io(part(""), part("")) + assert_nil io.read(1) + end + + def test_multipart_read_all + io = composite_io(part("abcd"), part("1234")) + assert_equal 8, io.length + assert_equal "abcd1234", io.read + end + + def test_multipart_read_limited + io = composite_io(part("abcd"), part("1234")) + assert_equal "abc", io.read(3) + assert_equal "d12", io.read(3) + assert_equal "34", io.read(3) + assert_equal nil, io.read(3) + assert_equal nil, io.read(3) + end + + def test_multipart_read_limited_size_larger_than_part + io = composite_io(part("abcd"), part("1234")) + assert_equal "abcd12", io.read(6) + assert_equal "34", io.read(6) + assert_equal nil, io.read(6) + end + + def test_multipart_read_with_blank_parts + io = composite_io(part(""), part("abcd"), part(""), part("1234"), part("")) + assert_equal "abcd12", io.read(6) + assert_equal "34", io.read(6) + assert_equal nil, io.read(6) + end + + def test_multipart_rewind + io = composite_io(part("abcd"), part("1234")) + assert_equal "abc", io.read(3) + assert_equal "d12", io.read(3) + io.rewind + assert_equal "abc", io.read(3) + assert_equal "d1234", io.read(5) + assert_equal nil, io.read(3) + io.rewind + assert_equal "ab", io.read(2) + end + + if IO.respond_to?(:copy_stream) + def test_compatible_with_copy_stream + target_io = StringIO.new + io = composite_io(part("abcd"), part("1234")) + + Faraday::Timer.timeout(1) do + IO.copy_stream(io, target_io) + end + assert_equal "abcd1234", target_io.string + end + end + + unless RUBY_VERSION < '1.9' + def test_read_from_multibyte + File.open(File.dirname(__FILE__) + '/multibyte.txt') do |utf8| + io = composite_io(part("\x86"), Part.new(utf8)) + assert_equal bin("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n"), io.read + end + end + + def test_limited_from_multibyte + File.open(File.dirname(__FILE__) + '/multibyte.txt') do |utf8| + io = composite_io(part("\x86"), Part.new(utf8)) + assert_equal bin("\x86\xE3\x83"), io.read(3) + assert_equal bin("\x95\xE3\x82"), io.read(3) + assert_equal bin("\xA1\xE3\x82\xA4\xE3\x83\xAB\n"), io.read(8) + end + end + end + + def bin(str) + str.force_encoding("BINARY") if str.respond_to?(:force_encoding) + str + end +end diff --git a/test/multibyte.txt b/test/multibyte.txt new file mode 100644 index 00000000..24a84b04 --- /dev/null +++ b/test/multibyte.txt @@ -0,0 +1 @@ +ファイル