diff --git a/.gitignore b/.gitignore index c02a141..3290571 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ pkg *.swo *.swp /Gemfile.lock +/.rspec_status diff --git a/lib/composite_io.rb b/lib/composite_io.rb deleted file mode 100644 index 7fcdc70..0000000 --- a/lib/composite_io.rb +++ /dev/null @@ -1,108 +0,0 @@ -#-- -# Copyright (c) 2007-2012 Nick Sieger. -# See the file README.txt included with the distribution for -# software license details. -#++ - -# Concatenate together multiple IO objects into a single, composite IO object -# for purposes of reading as a single stream. -# -# @example -# crio = CompositeReadIO.new(StringIO.new('one'), -# StringIO.new('two'), -# StringIO.new('three')) -# puts crio.read # => "onetwothree" -class CompositeReadIO - # Create a new composite-read IO from the arguments, all of which should - # respond to #read in a manner consistent with IO. - def initialize(*ios) - @ios = ios.flatten - @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 - - def rewind - @ios.each { |io| io.rewind } - @index = 0 - end - - private - - def current_io - @ios[@index] - end - - def advance_io - @index += 1 - end -end - -# Convenience methods for dealing with files and IO that are to be uploaded. -class UploadIO - attr_reader :content_type, :original_filename, :local_path, :io, :opts - - # Create an upload IO suitable for including in the params hash of a - # Net::HTTP::Post::Multipart. - # - # Can take two forms. The first accepts a filename and content type, and - # opens the file for reading (to be closed by finalizer). - # - # The second accepts an already-open IO, but also requires a third argument, - # the filename from which it was opened (particularly useful/recommended if - # uploading directly from a form in a framework, which often save the file to - # an arbitrarily named RackMultipart file in /tmp). - # - # @example - # UploadIO.new("file.txt", "text/plain") - # UploadIO.new(file_io, "text/plain", "file.txt") - def initialize(filename_or_io, content_type, filename = nil, opts = {}) - io = filename_or_io - local_path = "" - if io.respond_to? :read - # in Ruby 1.9.2, StringIOs no longer respond to path - # (since they respond to :length, so we don't need their local path, see parts.rb:41) - local_path = filename_or_io.respond_to?(:path) ? filename_or_io.path : "local.path" - else - io = File.open(filename_or_io) - local_path = filename_or_io - end - filename ||= local_path - - @content_type = content_type - @original_filename = File.basename(filename) - @local_path = local_path - @io = io - @opts = opts - end - - def self.convert!(io, content_type, original_filename, local_path) - raise ArgumentError, "convert! has been removed. You must now wrap IOs " \ - "using:\nUploadIO.new(filename_or_io, content_type, " \ - "filename=nil)\nPlease update your code." - end - - def method_missing(*args) - @io.send(*args) - end - - def respond_to?(meth, include_all = false) - @io.respond_to?(meth, include_all) || super(meth, include_all) - end -end diff --git a/lib/multipart_post.rb b/lib/multipart.rb similarity index 77% rename from lib/multipart_post.rb rename to lib/multipart.rb index 3a91cde..63bbf6e 100644 --- a/lib/multipart_post.rb +++ b/lib/multipart.rb @@ -4,6 +4,5 @@ # software license details. #++ -module MultipartPost - VERSION = "2.1.1" +module Multipart end diff --git a/lib/multipart/post.rb b/lib/multipart/post.rb new file mode 100644 index 0000000..f726be1 --- /dev/null +++ b/lib/multipart/post.rb @@ -0,0 +1,4 @@ +module Multipart + module Post + end +end diff --git a/lib/multipart/post/composite_read_io.rb b/lib/multipart/post/composite_read_io.rb new file mode 100644 index 0000000..2e45fc3 --- /dev/null +++ b/lib/multipart/post/composite_read_io.rb @@ -0,0 +1,112 @@ +#-- +# Copyright (c) 2007-2012 Nick Sieger. +# See the file README.txt included with the distribution for +# software license details. +#++ + +# Concatenate together multiple IO objects into a single, composite IO object +# for purposes of reading as a single stream. +# +# @example +# crio = CompositeReadIO.new(StringIO.new('one'), +# StringIO.new('two'), +# StringIO.new('three')) +# puts crio.read # => "onetwothree" +module Multipart + module Post + class CompositeReadIO + # Create a new composite-read IO from the arguments, all of which should + # respond to #read in a manner consistent with IO. + def initialize(*ios) + @ios = ios.flatten + @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 + + def rewind + @ios.each { |io| io.rewind } + @index = 0 + end + + private + + def current_io + @ios[@index] + end + + def advance_io + @index += 1 + end + end + + # Convenience methods for dealing with files and IO that are to be uploaded. + class UploadIO + attr_reader :content_type, :original_filename, :local_path, :io, :opts + + # Create an upload IO suitable for including in the params hash of a + # Net::HTTP::Post::Multipart. + # + # Can take two forms. The first accepts a filename and content type, and + # opens the file for reading (to be closed by finalizer). + # + # The second accepts an already-open IO, but also requires a third argument, + # the filename from which it was opened (particularly useful/recommended if + # uploading directly from a form in a framework, which often save the file to + # an arbitrarily named RackMultipart file in /tmp). + # + # @example + # UploadIO.new("file.txt", "text/plain") + # UploadIO.new(file_io, "text/plain", "file.txt") + def initialize(filename_or_io, content_type, filename = nil, opts = {}) + io = filename_or_io + local_path = "" + if io.respond_to? :read + # in Ruby 1.9.2, StringIOs no longer respond to path + # (since they respond to :length, so we don't need their local path, see parts.rb:41) + local_path = filename_or_io.respond_to?(:path) ? filename_or_io.path : "local.path" + else + io = File.open(filename_or_io) + local_path = filename_or_io + end + filename ||= local_path + + @content_type = content_type + @original_filename = File.basename(filename) + @local_path = local_path + @io = io + @opts = opts + end + + def self.convert!(io, content_type, original_filename, local_path) + raise ArgumentError, "convert! has been removed. You must now wrap IOs " \ + "using:\nUploadIO.new(filename_or_io, content_type, " \ + "filename=nil)\nPlease update your code." + end + + def method_missing(*args) + @io.send(*args) + end + + def respond_to?(meth, include_all = false) + @io.respond_to?(meth, include_all) || super(meth, include_all) + end + end + end +end diff --git a/lib/multipart/post/multipartable.rb b/lib/multipart/post/multipartable.rb new file mode 100644 index 0000000..27ba8ed --- /dev/null +++ b/lib/multipart/post/multipartable.rb @@ -0,0 +1,53 @@ +#-- +# Copyright (c) 2007-2013 Nick Sieger. +# See the file README.txt included with the distribution for +# software license details. +#++ + +require 'multipart/post/parts' +require 'multipart/post/composite_read_io' +require 'securerandom' + +module Multipart + module Post + module Multipartable + def self.secure_boundary + # https://tools.ietf.org/html/rfc7230 + # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + # / DIGIT / ALPHA + + # https://tools.ietf.org/html/rfc2046 + # bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / + # "+" / "_" / "," / "-" / "." / + # "/" / ":" / "=" / "?" + + "--#{SecureRandom.uuid}" + end + + def initialize(path, params, headers={}, boundary = Multipartable.secure_boundary) + headers = headers.clone # don't want to modify the original variable + parts_headers = headers.delete(:parts) || {} + super(path, headers) + parts = params.map do |k,v| + case v + when Array + v.map {|item| Parts::Part.new(boundary, k, item, parts_headers[k]) } + else + Parts::Part.new(boundary, k, v, parts_headers[k]) + end + end.flatten + parts << Parts::EpiloguePart.new(boundary) + ios = parts.map {|p| p.to_io } + self.set_content_type(headers["Content-Type"] || "multipart/form-data", + { "boundary" => boundary }) + self.content_length = parts.inject(0) {|sum,i| sum + i.length } + self.body_stream = CompositeReadIO.new(*ios) + + @boundary = boundary + end + + attr :boundary + end + end +end diff --git a/lib/multipart/post/parts.rb b/lib/multipart/post/parts.rb new file mode 100644 index 0000000..f14f80b --- /dev/null +++ b/lib/multipart/post/parts.rb @@ -0,0 +1,131 @@ +#-- +# Copyright (c) 2007-2013 Nick Sieger. +# See the file README.txt included with the distribution for +# software license details. +#++ + +module Multipart + module Post + module Parts + module Part + def self.new(boundary, name, value, headers = {}) + headers ||= {} # avoid nil values + if file?(value) + FilePart.new(boundary, name, value, headers) + else + ParamPart.new(boundary, name, value, headers) + end + end + + def self.file?(value) + value.respond_to?(:content_type) && value.respond_to?(:original_filename) + end + + def length + @part.length + end + + def to_io + @io + end + end + + # Represents a parametric part to be filled with given value. + class ParamPart + include Part + + # @param boundary [String] + # @param name [#to_s] + # @param value [String] + # @param headers [Hash] Content-Type and Content-ID are used, if present. + def initialize(boundary, name, value, headers = {}) + @part = build_part(boundary, name, value, headers) + @io = StringIO.new(@part) + end + + def length + @part.bytesize + end + + # @param boundary [String] + # @param name [#to_s] + # @param value [String] + # @param headers [Hash] Content-Type is used, if present. + def build_part(boundary, name, value, headers = {}) + part = '' + part << "--#{boundary}\r\n" + part << "Content-ID: #{headers["Content-ID"]}\r\n" if headers["Content-ID"] + part << "Content-Disposition: form-data; name=\"#{name.to_s}\"\r\n" + part << "Content-Type: #{headers["Content-Type"]}\r\n" if headers["Content-Type"] + part << "\r\n" + part << "#{value}\r\n" + end + end + + # Represents a part to be filled from file IO. + class FilePart + include Part + + attr_reader :length + + # @param boundary [String] + # @param name [#to_s] + # @param io [IO] + # @param headers [Hash] + def initialize(boundary, name, io, headers = {}) + file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path) + @head = build_head(boundary, name, io.original_filename, io.content_type, file_length, + io.respond_to?(:opts) ? io.opts.merge(headers) : headers) + @foot = "\r\n" + @length = @head.bytesize + file_length + @foot.length + @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new(@foot)) + end + + # @param boundary [String] + # @param name [#to_s] + # @param filename [String] + # @param type [String] + # @param content_len [Integer] + # @param opts [Hash] + def build_head(boundary, name, filename, type, content_len, opts = {}) + opts = opts.clone + + trans_encoding = opts.delete("Content-Transfer-Encoding") || "binary" + content_disposition = opts.delete("Content-Disposition") || "form-data" + + part = '' + part << "--#{boundary}\r\n" + part << "Content-Disposition: #{content_disposition}; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n" + part << "Content-Length: #{content_len}\r\n" + if content_id = opts.delete("Content-ID") + part << "Content-ID: #{content_id}\r\n" + end + + if opts["Content-Type"] != nil + part << "Content-Type: " + opts["Content-Type"] + "\r\n" + else + part << "Content-Type: #{type}\r\n" + end + + part << "Content-Transfer-Encoding: #{trans_encoding}\r\n" + + opts.each do |k, v| + part << "#{k}: #{v}\r\n" + end + + part << "\r\n" + end + end + + # Represents the epilogue or closing boundary. + class EpiloguePart + include Part + + def initialize(boundary) + @part = "--#{boundary}--\r\n" + @io = StringIO.new(@part) + end + end + end + end +end diff --git a/lib/multipart/post/version.rb b/lib/multipart/post/version.rb new file mode 100644 index 0000000..cb5120c --- /dev/null +++ b/lib/multipart/post/version.rb @@ -0,0 +1,5 @@ +module Multipart + module Post + VERSION = "2.1.1" + end +end diff --git a/lib/multipartable.rb b/lib/multipartable.rb deleted file mode 100644 index 6a96575..0000000 --- a/lib/multipartable.rb +++ /dev/null @@ -1,48 +0,0 @@ -#-- -# Copyright (c) 2007-2013 Nick Sieger. -# See the file README.txt included with the distribution for -# software license details. -#++ - -require 'parts' -require 'securerandom' - -module Multipartable - def self.secure_boundary - # https://tools.ietf.org/html/rfc7230 - # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - # / DIGIT / ALPHA - - # https://tools.ietf.org/html/rfc2046 - # bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / - # "+" / "_" / "," / "-" / "." / - # "/" / ":" / "=" / "?" - - "--#{SecureRandom.uuid}" - end - - def initialize(path, params, headers={}, boundary = Multipartable.secure_boundary) - headers = headers.clone # don't want to modify the original variable - parts_headers = headers.delete(:parts) || {} - super(path, headers) - parts = params.map do |k,v| - case v - when Array - v.map {|item| Parts::Part.new(boundary, k, item, parts_headers[k]) } - else - Parts::Part.new(boundary, k, v, parts_headers[k]) - end - end.flatten - parts << Parts::EpiloguePart.new(boundary) - ios = parts.map {|p| p.to_io } - self.set_content_type(headers["Content-Type"] || "multipart/form-data", - { "boundary" => boundary }) - self.content_length = parts.inject(0) {|sum,i| sum + i.length } - self.body_stream = CompositeReadIO.new(*ios) - - @boundary = boundary - end - - attr :boundary -end diff --git a/lib/net/http/post/multipart.rb b/lib/net/http/post/multipart.rb index dc53599..ac56097 100644 --- a/lib/net/http/post/multipart.rb +++ b/lib/net/http/post/multipart.rb @@ -7,21 +7,21 @@ require 'net/http' require 'stringio' require 'cgi' -require 'composite_io' -require 'multipartable' -require 'parts' +require 'multipart/post/parts' +require 'multipart/post/composite_read_io' +require 'multipart/post/multipartable' module Net class HTTP class Put class Multipart < Put - include Multipartable + include ::Multipart::Post::Multipartable end end class Post class Multipart < Post - include Multipartable + include ::Multipart::Post::Multipartable end end end diff --git a/lib/parts.rb b/lib/parts.rb deleted file mode 100644 index 3382a18..0000000 --- a/lib/parts.rb +++ /dev/null @@ -1,127 +0,0 @@ -#-- -# Copyright (c) 2007-2013 Nick Sieger. -# See the file README.txt included with the distribution for -# software license details. -#++ - -module Parts - module Part - def self.new(boundary, name, value, headers = {}) - headers ||= {} # avoid nil values - if file?(value) - FilePart.new(boundary, name, value, headers) - else - ParamPart.new(boundary, name, value, headers) - end - end - - def self.file?(value) - value.respond_to?(:content_type) && value.respond_to?(:original_filename) - end - - def length - @part.length - end - - def to_io - @io - end - end - - # Represents a parametric part to be filled with given value. - class ParamPart - include Part - - # @param boundary [String] - # @param name [#to_s] - # @param value [String] - # @param headers [Hash] Content-Type and Content-ID are used, if present. - def initialize(boundary, name, value, headers = {}) - @part = build_part(boundary, name, value, headers) - @io = StringIO.new(@part) - end - - def length - @part.bytesize - end - - # @param boundary [String] - # @param name [#to_s] - # @param value [String] - # @param headers [Hash] Content-Type is used, if present. - def build_part(boundary, name, value, headers = {}) - part = '' - part << "--#{boundary}\r\n" - part << "Content-ID: #{headers["Content-ID"]}\r\n" if headers["Content-ID"] - part << "Content-Disposition: form-data; name=\"#{name.to_s}\"\r\n" - part << "Content-Type: #{headers["Content-Type"]}\r\n" if headers["Content-Type"] - part << "\r\n" - part << "#{value}\r\n" - end - end - - # Represents a part to be filled from file IO. - class FilePart - include Part - - attr_reader :length - - # @param boundary [String] - # @param name [#to_s] - # @param io [IO] - # @param headers [Hash] - def initialize(boundary, name, io, headers = {}) - file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path) - @head = build_head(boundary, name, io.original_filename, io.content_type, file_length, - io.respond_to?(:opts) ? io.opts.merge(headers) : headers) - @foot = "\r\n" - @length = @head.bytesize + file_length + @foot.length - @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new(@foot)) - end - - # @param boundary [String] - # @param name [#to_s] - # @param filename [String] - # @param type [String] - # @param content_len [Integer] - # @param opts [Hash] - def build_head(boundary, name, filename, type, content_len, opts = {}) - opts = opts.clone - - trans_encoding = opts.delete("Content-Transfer-Encoding") || "binary" - content_disposition = opts.delete("Content-Disposition") || "form-data" - - part = '' - part << "--#{boundary}\r\n" - part << "Content-Disposition: #{content_disposition}; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n" - part << "Content-Length: #{content_len}\r\n" - if content_id = opts.delete("Content-ID") - part << "Content-ID: #{content_id}\r\n" - end - - if opts["Content-Type"] != nil - part << "Content-Type: " + opts["Content-Type"] + "\r\n" - else - part << "Content-Type: #{type}\r\n" - end - - part << "Content-Transfer-Encoding: #{trans_encoding}\r\n" - - opts.each do |k, v| - part << "#{k}: #{v}\r\n" - end - - part << "\r\n" - end - end - - # Represents the epilogue or closing boundary. - class EpiloguePart - include Part - - def initialize(boundary) - @part = "--#{boundary}--\r\n" - @io = StringIO.new(@part) - end - end -end diff --git a/multipart-post.gemspec b/multipart-post.gemspec index a3377f5..ef5fed1 100644 --- a/multipart-post.gemspec +++ b/multipart-post.gemspec @@ -1,10 +1,10 @@ # -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) -require "multipart_post" +require "multipart/post/version" Gem::Specification.new do |spec| spec.name = "multipart-post" - spec.version = MultipartPost::VERSION + spec.version = Multipart::Post::VERSION spec.authors = ["Nick Sieger", "Samuel Williams"] spec.email = ["nick@nicksieger.com", "samuel.williams@oriontransfer.co.nz"] spec.homepage = "https://github.com/nicksieger/multipart-post" diff --git a/spec/multibyte.txt b/spec/fixtures/multibyte.txt similarity index 100% rename from spec/multibyte.txt rename to spec/fixtures/multibyte.txt diff --git a/spec/composite_io_spec.rb b/spec/multipart/post/composite_read_io_spec.rb similarity index 58% rename from spec/composite_io_spec.rb rename to spec/multipart/post/composite_read_io_spec.rb index c9409b3..5b03f7d 100644 --- a/spec/composite_io_spec.rb +++ b/spec/multipart/post/composite_read_io_spec.rb @@ -19,10 +19,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -require 'composite_io' +require 'multipart/post/composite_read_io' require 'stringio' require 'timeout' +MULTIBYTE = File.dirname(__FILE__) + '/../../fixtures/multibyte.txt' + RSpec.shared_context "composite io" do it "test_full_read_from_several_ios" do expect(subject.read).to be == 'the quick brown fox' @@ -80,59 +82,63 @@ end end -RSpec.describe CompositeReadIO do - describe "generic io" do - subject {StringIO.new('the quick brown fox')} - - include_context "composite io" - end - - describe "composite io" do - subject {CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick '), StringIO.new('brown '), StringIO.new('fox'))} - - include_context "composite io" - end - - describe "nested composite io" do - subject {CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')), StringIO.new('brown '), StringIO.new('fox'))} - - include_context "composite io" - end - - describe "unicode composite io" do - let(:utf8_io) {File.open(File.dirname(__FILE__)+'/multibyte.txt')} - let(:binary_io) {StringIO.new("\x86")} - - subject {CompositeReadIO.new(binary_io, utf8_io)} - - it "test_read_from_multibyte" do - expect(subject.read).to be == "\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n".b - end - end - - it "test_convert_error" do - expect do - UploadIO.convert!('tmp.txt', 'text/plain', 'tmp.txt', 'tmp.txt') - end.to raise_error(ArgumentError, /convert! has been removed/) - end - - it "test_empty" do - expect(subject.read).to be == "" - end +module Multipart + module Post + RSpec.describe CompositeReadIO do + describe "generic io" do + subject {StringIO.new('the quick brown fox')} - it "test_empty_limited" do - expect(subject.read(1)).to be_nil - end + include_context "composite io" + end - it "test_empty_parts" do - io = CompositeReadIO.new(StringIO.new, StringIO.new('the '), StringIO.new, StringIO.new('quick')) - expect(io.read(3)).to be == "the" - expect(io.read(3)).to be == " qu" - expect(io.read(3)).to be == "ick" - end + describe "composite io" do + subject {CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick '), StringIO.new('brown '), StringIO.new('fox'))} + + include_context "composite io" + end + + describe "nested composite io" do + subject {CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')), StringIO.new('brown '), StringIO.new('fox'))} + + include_context "composite io" + end + + describe "unicode composite io" do + let(:utf8_io) { File.open(MULTIBYTE) } + let(:binary_io) {StringIO.new("\x86")} - it "test_all_empty_parts" do - io = CompositeReadIO.new(StringIO.new, StringIO.new) - expect(io.read(1)).to be_nil + subject {CompositeReadIO.new(binary_io, utf8_io)} + + it "test_read_from_multibyte" do + expect(subject.read).to be == "\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n".b + end + end + + it "test_convert_error" do + expect do + UploadIO.convert!('tmp.txt', 'text/plain', 'tmp.txt', 'tmp.txt') + end.to raise_error(ArgumentError, /convert! has been removed/) + end + + it "test_empty" do + expect(subject.read).to be == "" + end + + it "test_empty_limited" do + expect(subject.read(1)).to be_nil + end + + it "test_empty_parts" do + io = CompositeReadIO.new(StringIO.new, StringIO.new('the '), StringIO.new, StringIO.new('quick')) + expect(io.read(3)).to be == "the" + expect(io.read(3)).to be == " qu" + expect(io.read(3)).to be == "ick" + end + + it "test_all_empty_parts" do + io = CompositeReadIO.new(StringIO.new, StringIO.new) + expect(io.read(1)).to be_nil + end + end end end diff --git a/spec/multipart/post/parts_spec.rb b/spec/multipart/post/parts_spec.rb new file mode 100644 index 0000000..5c5d286 --- /dev/null +++ b/spec/multipart/post/parts_spec.rb @@ -0,0 +1,111 @@ +# Copyright, 2012, by Nick Sieger. +# Copyright, 2017, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +require 'multipart/post/parts' +require 'multipart/post/composite_read_io' +require 'stringio' +require 'tempfile' + +MULTIBYTE = File.dirname(__FILE__) + '/../../fixtures/multibyte.txt' +TEMP_FILE = "temp.txt" + +module AssertPartLength + def assert_part_length(part) + bytes = part.to_io.read + bytesize = bytes.respond_to?(:bytesize) ? bytes.bytesize : bytes.length + expect(bytesize).to be == part.length + end +end + +module Multipart + module Post + RSpec.describe Parts do + let(:string_with_content_type) do + Class.new(String) do + def content_type; 'application/data'; end + end + end + + it "test_file_with_upload_io" do + expect(Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))).to be true + end + + it "test_file_with_modified_string" do + expect(Parts::Part.file?(string_with_content_type.new("Hello"))).to be false + end + + it "test_new_with_modified_string" do + expect(Parts::Part.new("boundary", "multibyte", string_with_content_type.new("Hello"))).to be_kind_of(Parts::ParamPart) + end + end + + RSpec.describe Parts::FilePart do + include AssertPartLength + + before(:each) do + File.open(TEMP_FILE, "w") {|f| f << "1234567890"} + io = UploadIO.new(TEMP_FILE, "text/plain") + @part = Parts::FilePart.new("boundary", "afile", io) + end + + after(:each) do + File.delete(TEMP_FILE) rescue nil + end + + it "test_correct_length" do + assert_part_length @part + end + + it "test_multibyte_file_length" do + assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(MULTIBYTE, "text/plain")) + end + + it "test_multibyte_filename" do + name = File.read(MULTIBYTE, 300) + file = Tempfile.new(name.respond_to?(:force_encoding) ? name.force_encoding("UTF-8") : name) + assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(file, "text/plain")) + file.close + end + + it "test_force_content_type_header" do + part = Parts::FilePart.new("boundary", "afile", UploadIO.new(TEMP_FILE, "text/plain"), { "Content-Type" => "application/pdf" }) + expect(part.to_io.read).to match(/Content-Type: application\/pdf/) + end + end + + RSpec.describe Parts::ParamPart do + include AssertPartLength + + before(:each) do + @part = Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE)) + end + + it "test_correct_length" do + assert_part_length @part + end + + it "test_content_id" do + part = Parts::ParamPart.new("boundary", "with_content_id", "foobar", "Content-ID" => "id") + expect(part.to_io.read).to match(/Content-ID: id/) + end + end + end +end diff --git a/spec/net/http/post/multipart_spec.rb b/spec/net/http/post/multipart_spec.rb index a19ea04..3d6002e 100644 --- a/spec/net/http/post/multipart_spec.rb +++ b/spec/net/http/post/multipart_spec.rb @@ -60,19 +60,19 @@ def assert_additional_headers_added(post, parts_headers) it "test_form_multipart_body" do File.open(TEMP_FILE, "w") {|f| f << "1234567890"} @io = File.open(TEMP_FILE) - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io) end it "test_form_multipart_body_with_stringio" do @io = StringIO.new("1234567890") - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io) end it "test_form_multiparty_body_with_parts_headers" do @io = StringIO.new("1234567890") - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE parts = { :text => 'bar', :file => @io } headers = { :parts => { @@ -89,7 +89,7 @@ def assert_additional_headers_added(post, parts_headers) it "test_form_multipart_body_with_array_value" do File.open(TEMP_FILE, "w") {|f| f << "1234567890"} @io = File.open(TEMP_FILE) - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE params = {:foo => ['bar', 'quux'], :file => @io} headers = { :parts => { :foo => { "Content-Type" => "application/json; charset=UTF-8" } } } @@ -106,7 +106,7 @@ def assert_additional_headers_added(post, parts_headers) it "test_form_multipart_body_with_arrayparam" do File.open(TEMP_FILE, "w") {|f| f << "1234567890"} @io = File.open(TEMP_FILE) - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :multivalueParam => ['bar','bah'], :file => @io) end end @@ -117,7 +117,7 @@ def assert_additional_headers_added(post, parts_headers) it "test_form_multipart_body_put" do File.open(TEMP_FILE, "w") {|f| f << "1234567890"} @io = File.open(TEMP_FILE) - @io = UploadIO.new @io, "text/plain", TEMP_FILE + @io = Multipart::Post::UploadIO.new @io, "text/plain", TEMP_FILE assert_results Net::HTTP::Put::Multipart.new("/foo/bar", :foo => 'bar', :file => @io) end end diff --git a/spec/parts_spec.rb b/spec/parts_spec.rb deleted file mode 100644 index b542c04..0000000 --- a/spec/parts_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright, 2012, by Nick Sieger. -# Copyright, 2017, by Samuel G. D. Williams. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -require 'parts' -require 'stringio' -require 'composite_io' -require 'tempfile' - -MULTIBYTE = File.dirname(__FILE__)+'/multibyte.txt' -TEMP_FILE = "temp.txt" - -module AssertPartLength - def assert_part_length(part) - bytes = part.to_io.read - bytesize = bytes.respond_to?(:bytesize) ? bytes.bytesize : bytes.length - expect(bytesize).to be == part.length - end -end - -RSpec.describe Parts do - let(:string_with_content_type) do - Class.new(String) do - def content_type; 'application/data'; end - end - end - - it "test_file_with_upload_io" do - expect(Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))).to be true - end - - it "test_file_with_modified_string" do - expect(Parts::Part.file?(string_with_content_type.new("Hello"))).to be false - end - - it "test_new_with_modified_string" do - expect(Parts::Part.new("boundary", "multibyte", string_with_content_type.new("Hello"))).to be_kind_of(Parts::ParamPart) - end -end - -RSpec.describe Parts::FilePart do - include AssertPartLength - - before(:each) do - File.open(TEMP_FILE, "w") {|f| f << "1234567890"} - io = UploadIO.new(TEMP_FILE, "text/plain") - @part = Parts::FilePart.new("boundary", "afile", io) - end - - after(:each) do - File.delete(TEMP_FILE) rescue nil - end - - it "test_correct_length" do - assert_part_length @part - end - - it "test_multibyte_file_length" do - assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(MULTIBYTE, "text/plain")) - end - - it "test_multibyte_filename" do - name = File.read(MULTIBYTE, 300) - file = Tempfile.new(name.respond_to?(:force_encoding) ? name.force_encoding("UTF-8") : name) - assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(file, "text/plain")) - file.close - end - - it "test_force_content_type_header" do - part = Parts::FilePart.new("boundary", "afile", UploadIO.new(TEMP_FILE, "text/plain"), { "Content-Type" => "application/pdf" }) - expect(part.to_io.read).to match(/Content-Type: application\/pdf/) - end -end - -RSpec.describe Parts::ParamPart do - include AssertPartLength - - before(:each) do - @part = Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE)) - end - - it "test_correct_length" do - assert_part_length @part - end - - it "test_content_id" do - part = Parts::ParamPart.new("boundary", "with_content_id", "foobar", "Content-ID" => "id") - expect(part.to_io.read).to match(/Content-ID: id/) - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c52cf78..6082995 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,7 +17,7 @@ end require "bundler/setup" -require "multipart_post" +require "multipart/post" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure