From 477a59872cb6c65dd5b10cb1d8496a364e456e1c Mon Sep 17 00:00:00 2001 From: Patrick Davey Date: Thu, 26 Sep 2019 11:25:23 +1200 Subject: [PATCH 1/4] Add rspec_status to gitignore With current version of rspec with rspec_status file is generated when the tests are run, ignoring in version control. --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 079158920b196da380ca24456f835ec021686b79 Mon Sep 17 00:00:00 2001 From: Patrick Davey Date: Thu, 10 Oct 2019 20:08:13 +1300 Subject: [PATCH 2/4] Refactor namespaces used. This changes the namespace for the gem from MultipartPost into Multipart::Post to match the actual name of the gem. This also extracts out of the global namespace the Parts module to live instead under Multipart::Post::Parts --- lib/{multipart_post.rb => multipart.rb} | 3 +- lib/multipart/post.rb | 4 + lib/multipart/post/parts.rb | 131 ++++++++++++++++++++++++ lib/multipart/post/version.rb | 5 + lib/multipartable.rb | 8 +- lib/net/http/post/multipart.rb | 2 +- lib/parts.rb | 127 ----------------------- multipart-post.gemspec | 4 +- spec/composite_io_spec.rb | 2 +- spec/{ => fixtures}/multibyte.txt | 0 spec/{ => multipart/post}/parts_spec.rb | 28 ++--- spec/spec_helper.rb | 2 +- 12 files changed, 164 insertions(+), 152 deletions(-) rename lib/{multipart_post.rb => multipart.rb} (77%) create mode 100644 lib/multipart/post.rb create mode 100644 lib/multipart/post/parts.rb create mode 100644 lib/multipart/post/version.rb delete mode 100644 lib/parts.rb rename spec/{ => fixtures}/multibyte.txt (100%) rename spec/{ => multipart/post}/parts_spec.rb (67%) 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/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 index 6a96575..f1d7a9d 100644 --- a/lib/multipartable.rb +++ b/lib/multipartable.rb @@ -4,7 +4,7 @@ # software license details. #++ -require 'parts' +require 'multipart/post/parts' require 'securerandom' module Multipartable @@ -29,12 +29,12 @@ def initialize(path, params, headers={}, boundary = Multipartable.secure_boundar parts = params.map do |k,v| case v when Array - v.map {|item| Parts::Part.new(boundary, k, item, parts_headers[k]) } + v.map {|item| Multipart::Post::Parts::Part.new(boundary, k, item, parts_headers[k]) } else - Parts::Part.new(boundary, k, v, parts_headers[k]) + Multipart::Post::Parts::Part.new(boundary, k, v, parts_headers[k]) end end.flatten - parts << Parts::EpiloguePart.new(boundary) + parts << Multipart::Post::Parts::EpiloguePart.new(boundary) ios = parts.map {|p| p.to_io } self.set_content_type(headers["Content-Type"] || "multipart/form-data", { "boundary" => boundary }) diff --git a/lib/net/http/post/multipart.rb b/lib/net/http/post/multipart.rb index dc53599..0cac23f 100644 --- a/lib/net/http/post/multipart.rb +++ b/lib/net/http/post/multipart.rb @@ -9,7 +9,7 @@ require 'cgi' require 'composite_io' require 'multipartable' -require 'parts' +require 'multipart/post/parts' module Net class HTTP 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/composite_io_spec.rb b/spec/composite_io_spec.rb index c9409b3..17c25e9 100644 --- a/spec/composite_io_spec.rb +++ b/spec/composite_io_spec.rb @@ -100,7 +100,7 @@ end describe "unicode composite io" do - let(:utf8_io) {File.open(File.dirname(__FILE__)+'/multibyte.txt')} + let(:utf8_io) {File.open(File.dirname(__FILE__) + '/fixtures/multibyte.txt')} let(:binary_io) {StringIO.new("\x86")} subject {CompositeReadIO.new(binary_io, utf8_io)} 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/parts_spec.rb b/spec/multipart/post/parts_spec.rb similarity index 67% rename from spec/parts_spec.rb rename to spec/multipart/post/parts_spec.rb index b542c04..7b257e5 100644 --- a/spec/parts_spec.rb +++ b/spec/multipart/post/parts_spec.rb @@ -19,12 +19,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -require 'parts' +require 'multipart/post/parts' require 'stringio' require 'composite_io' require 'tempfile' -MULTIBYTE = File.dirname(__FILE__)+'/multibyte.txt' +MULTIBYTE = File.dirname(__FILE__) + '/../../fixtures/multibyte.txt' TEMP_FILE = "temp.txt" module AssertPartLength @@ -35,7 +35,7 @@ def assert_part_length(part) end end -RSpec.describe Parts do +RSpec.describe Multipart::Post::Parts do let(:string_with_content_type) do Class.new(String) do def content_type; 'application/data'; end @@ -43,25 +43,25 @@ def content_type; 'application/data'; end end it "test_file_with_upload_io" do - expect(Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))).to be true + expect(Multipart::Post::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 + expect(Multipart::Post::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) + expect(Multipart::Post::Parts::Part.new("boundary", "multibyte", string_with_content_type.new("Hello"))).to be_kind_of(Multipart::Post::Parts::ParamPart) end end -RSpec.describe Parts::FilePart do +RSpec.describe Multipart::Post::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) + @part = Multipart::Post::Parts::FilePart.new("boundary", "afile", io) end after(:each) do @@ -73,27 +73,27 @@ def content_type; 'application/data'; end end it "test_multibyte_file_length" do - assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(MULTIBYTE, "text/plain")) + assert_part_length Multipart::Post::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")) + assert_part_length Multipart::Post::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" }) + part = Multipart::Post::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 +RSpec.describe Multipart::Post::Parts::ParamPart do include AssertPartLength before(:each) do - @part = Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE)) + @part = Multipart::Post::Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE)) end it "test_correct_length" do @@ -101,7 +101,7 @@ def content_type; 'application/data'; end end it "test_content_id" do - part = Parts::ParamPart.new("boundary", "with_content_id", "foobar", "Content-ID" => "id") + part = Multipart::Post::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 From f3c5ea4fbb47c75cbf33d1c7d7c9b9be130051be Mon Sep 17 00:00:00 2001 From: Patrick Davey Date: Tue, 29 Oct 2019 19:11:12 +1300 Subject: [PATCH 3/4] Move CompositeReadIO into namespace Just moves the CompositeReadIO out into the Multipart::Post namespace. --- lib/composite_io.rb | 108 --------------- lib/multipart/post/composite_read_io.rb | 112 +++++++++++++++ lib/multipartable.rb | 3 +- lib/net/http/post/multipart.rb | 2 +- .../post/composite_read_io_spec.rb} | 110 ++++++++------- spec/multipart/post/parts_spec.rb | 130 +++++++++--------- spec/net/http/post/multipart_spec.rb | 12 +- 7 files changed, 246 insertions(+), 231 deletions(-) delete mode 100644 lib/composite_io.rb create mode 100644 lib/multipart/post/composite_read_io.rb rename spec/{composite_io_spec.rb => multipart/post/composite_read_io_spec.rb} (58%) 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/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/multipartable.rb b/lib/multipartable.rb index f1d7a9d..6efa9a8 100644 --- a/lib/multipartable.rb +++ b/lib/multipartable.rb @@ -5,6 +5,7 @@ #++ require 'multipart/post/parts' +require 'multipart/post/composite_read_io' require 'securerandom' module Multipartable @@ -39,7 +40,7 @@ def initialize(path, params, headers={}, boundary = Multipartable.secure_boundar 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) + self.body_stream = Multipart::Post::CompositeReadIO.new(*ios) @boundary = boundary end diff --git a/lib/net/http/post/multipart.rb b/lib/net/http/post/multipart.rb index 0cac23f..8e7e7f8 100644 --- a/lib/net/http/post/multipart.rb +++ b/lib/net/http/post/multipart.rb @@ -7,9 +7,9 @@ require 'net/http' require 'stringio' require 'cgi' -require 'composite_io' require 'multipartable' require 'multipart/post/parts' +require 'multipart/post/composite_read_io' module Net class HTTP 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 17c25e9..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__) + '/fixtures/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 index 7b257e5..5c5d286 100644 --- a/spec/multipart/post/parts_spec.rb +++ b/spec/multipart/post/parts_spec.rb @@ -20,8 +20,8 @@ # THE SOFTWARE. require 'multipart/post/parts' +require 'multipart/post/composite_read_io' require 'stringio' -require 'composite_io' require 'tempfile' MULTIBYTE = File.dirname(__FILE__) + '/../../fixtures/multibyte.txt' @@ -35,73 +35,77 @@ def assert_part_length(part) end end -RSpec.describe Multipart::Post::Parts do - let(:string_with_content_type) do - Class.new(String) do - def content_type; 'application/data'; 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 - end - - it "test_file_with_upload_io" do - expect(Multipart::Post::Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))).to be true - end - - it "test_file_with_modified_string" do - expect(Multipart::Post::Parts::Part.file?(string_with_content_type.new("Hello"))).to be false - end - - it "test_new_with_modified_string" do - expect(Multipart::Post::Parts::Part.new("boundary", "multibyte", string_with_content_type.new("Hello"))).to be_kind_of(Multipart::Post::Parts::ParamPart) - end -end - -RSpec.describe Multipart::Post::Parts::FilePart do - include AssertPartLength - - before(:each) do - File.open(TEMP_FILE, "w") {|f| f << "1234567890"} - io = UploadIO.new(TEMP_FILE, "text/plain") - @part = Multipart::Post::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 Multipart::Post::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 Multipart::Post::Parts::FilePart.new("boundary", "multibyte", UploadIO.new(file, "text/plain")) - file.close - end - - it "test_force_content_type_header" do - part = Multipart::Post::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::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 Multipart::Post::Parts::ParamPart do - include AssertPartLength + RSpec.describe Parts::ParamPart do + include AssertPartLength - before(:each) do - @part = Multipart::Post::Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE)) - end + 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_correct_length" do + assert_part_length @part + end - it "test_content_id" do - part = Multipart::Post::Parts::ParamPart.new("boundary", "with_content_id", "foobar", "Content-ID" => "id") - expect(part.to_io.read).to match(/Content-ID: id/) + 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 From b559dc5bcb4344e972b1e3eb68c2bbccb4daf68c Mon Sep 17 00:00:00 2001 From: Patrick Davey Date: Tue, 29 Oct 2019 19:20:11 +1300 Subject: [PATCH 4/4] Move multipartable out into Multipart namespace This is the last of the top level namespace polluting modules moved out. The tests are all passing (however, I can't comment on how complete the test coverage is) --- lib/multipart/post/multipartable.rb | 53 +++++++++++++++++++++++++++++ lib/multipartable.rb | 49 -------------------------- lib/net/http/post/multipart.rb | 6 ++-- 3 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 lib/multipart/post/multipartable.rb delete mode 100644 lib/multipartable.rb 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/multipartable.rb b/lib/multipartable.rb deleted file mode 100644 index 6efa9a8..0000000 --- a/lib/multipartable.rb +++ /dev/null @@ -1,49 +0,0 @@ -#-- -# 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 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| Multipart::Post::Parts::Part.new(boundary, k, item, parts_headers[k]) } - else - Multipart::Post::Parts::Part.new(boundary, k, v, parts_headers[k]) - end - end.flatten - parts << Multipart::Post::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 = Multipart::Post::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 8e7e7f8..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 'multipartable' 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