multipart-post 1.1.5 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,14 +11,15 @@ require 'composite_io'
11
11
  require 'multipartable'
12
12
  require 'parts'
13
13
 
14
- module Net #:nodoc:
15
- class HTTP #:nodoc:
14
+ module Net
15
+ class HTTP
16
16
  class Put
17
17
  class Multipart < Put
18
18
  include Multipartable
19
19
  end
20
20
  end
21
- class Post #:nodoc:
21
+
22
+ class Post
22
23
  class Multipart < Post
23
24
  include Multipartable
24
25
  end
@@ -1,19 +1,24 @@
1
1
  #--
2
- # Copyright (c) 2007-2012 Nick Sieger.
2
+ # Copyright (c) 2007-2013 Nick Sieger.
3
3
  # See the file README.txt included with the distribution for
4
4
  # software license details.
5
5
  #++
6
6
 
7
7
  module Parts
8
- module Part #:nodoc:
9
- def self.new(boundary, name, value)
10
- if value.respond_to? :content_type
11
- FilePart.new(boundary, name, value)
8
+ module Part
9
+ def self.new(boundary, name, value, headers = {})
10
+ headers ||= {} # avoid nil values
11
+ if file?(value)
12
+ FilePart.new(boundary, name, value, headers)
12
13
  else
13
- ParamPart.new(boundary, name, value)
14
+ ParamPart.new(boundary, name, value, headers)
14
15
  end
15
16
  end
16
17
 
18
+ def self.file?(value)
19
+ value.respond_to?(:content_type) && value.respond_to?(:original_filename)
20
+ end
21
+
17
22
  def length
18
23
  @part.length
19
24
  end
@@ -23,21 +28,32 @@ module Parts
23
28
  end
24
29
  end
25
30
 
31
+ # Represents a parametric part to be filled with given value.
26
32
  class ParamPart
27
33
  include Part
28
- def initialize(boundary, name, value)
29
- @part = build_part(boundary, name, value)
34
+
35
+ # @param boundary [String]
36
+ # @param name [#to_s]
37
+ # @param value [String]
38
+ # @param headers [Hash] Content-Type is used, if present.
39
+ def initialize(boundary, name, value, headers = {})
40
+ @part = build_part(boundary, name, value, headers)
30
41
  @io = StringIO.new(@part)
31
42
  end
32
43
 
33
44
  def length
34
45
  @part.bytesize
35
- end
46
+ end
36
47
 
37
- def build_part(boundary, name, value)
48
+ # @param boundary [String]
49
+ # @param name [#to_s]
50
+ # @param value [String]
51
+ # @param headers [Hash] Content-Type is used, if present.
52
+ def build_part(boundary, name, value, headers = {})
38
53
  part = ''
39
54
  part << "--#{boundary}\r\n"
40
55
  part << "Content-Disposition: form-data; name=\"#{name.to_s}\"\r\n"
56
+ part << "Content-Type: #{headers["Content-Type"]}\r\n" if headers["Content-Type"]
41
57
  part << "\r\n"
42
58
  part << "#{value}\r\n"
43
59
  end
@@ -46,29 +62,54 @@ module Parts
46
62
  # Represents a part to be filled from file IO.
47
63
  class FilePart
48
64
  include Part
65
+
49
66
  attr_reader :length
50
- def initialize(boundary, name, io)
67
+
68
+ # @param boundary [String]
69
+ # @param name [#to_s]
70
+ # @param io [IO]
71
+ # @param headers [Hash]
72
+ def initialize(boundary, name, io, headers = {})
51
73
  file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path)
52
74
  @head = build_head(boundary, name, io.original_filename, io.content_type, file_length,
53
- io.respond_to?(:opts) ? io.opts : {})
75
+ io.respond_to?(:opts) ? io.opts.merge(headers) : headers)
54
76
  @foot = "\r\n"
55
- @length = @head.length + file_length + @foot.length
77
+ @length = @head.bytesize + file_length + @foot.length
56
78
  @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new(@foot))
57
79
  end
58
80
 
81
+ # @param boundary [String]
82
+ # @param name [#to_s]
83
+ # @param filename [String]
84
+ # @param type [String]
85
+ # @param content_len [Integer]
86
+ # @param opts [Hash]
59
87
  def build_head(boundary, name, filename, type, content_len, opts = {})
60
- trans_encoding = opts["Content-Transfer-Encoding"] || "binary"
61
- content_disposition = opts["Content-Disposition"] || "form-data"
88
+ opts = opts.clone
89
+
90
+ trans_encoding = opts.delete("Content-Transfer-Encoding") || "binary"
91
+ content_disposition = opts.delete("Content-Disposition") || "form-data"
62
92
 
63
93
  part = ''
64
94
  part << "--#{boundary}\r\n"
65
95
  part << "Content-Disposition: #{content_disposition}; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n"
66
96
  part << "Content-Length: #{content_len}\r\n"
67
- if content_id = opts["Content-ID"]
97
+ if content_id = opts.delete("Content-ID")
68
98
  part << "Content-ID: #{content_id}\r\n"
69
99
  end
70
- part << "Content-Type: #{type}\r\n"
100
+
101
+ if opts["Content-Type"] != nil
102
+ part << "Content-Type: " + opts["Content-Type"] + "\r\n"
103
+ else
104
+ part << "Content-Type: #{type}\r\n"
105
+ end
106
+
71
107
  part << "Content-Transfer-Encoding: #{trans_encoding}\r\n"
108
+
109
+ opts.each do |k, v|
110
+ part << "#{k}: #{v}\r\n"
111
+ end
112
+
72
113
  part << "\r\n"
73
114
  end
74
115
  end
@@ -76,8 +117,9 @@ module Parts
76
117
  # Represents the epilogue or closing boundary.
77
118
  class EpiloguePart
78
119
  include Part
120
+
79
121
  def initialize(boundary)
80
- @part = "--#{boundary}--\r\n\r\n"
122
+ @part = "--#{boundary}--\r\n"
81
123
  @io = StringIO.new(@part)
82
124
  end
83
125
  end
@@ -2,19 +2,22 @@
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
3
  require "multipart_post"
4
4
 
5
- Gem::Specification.new do |s|
6
- s.name = "multipart-post"
7
- s.version = MultipartPost::VERSION
8
- s.authors = ["Nick Sieger"]
9
- s.email = ["nick@nicksieger.com"]
10
- s.homepage = "https://github.com/nicksieger/multipart-post"
11
- s.summary = %q{A multipart form post accessory for Net::HTTP.}
12
- s.description = %q{Use with Net::HTTP to do multipart form posts. IO values that have #content_type, #original_filename, and #local_path will be posted as a binary file.}
13
-
14
- s.rubyforge_project = "caldersphere"
15
-
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "multipart-post"
7
+ spec.version = MultipartPost::VERSION
8
+ spec.authors = ["Nick Sieger", "Samuel Williams"]
9
+ spec.email = ["nick@nicksieger.com", "samuel.williams@oriontransfer.co.nz"]
10
+ spec.homepage = "https://github.com/nicksieger/multipart-post"
11
+ spec.summary = %q{A multipart form post accessory for Net::HTTP.}
12
+ spec.license = "MIT"
13
+ spec.description = %q{Use with Net::HTTP to do multipart form postspec. IO values that have #content_type, #original_filename, and #local_path will be posted as a binary file.}
14
+
15
+ spec.files = `git ls-files`.split("\n")
16
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency 'bundler', ['>= 1.3', '< 3']
21
+ spec.add_development_dependency 'rspec', '~> 3.4'
22
+ spec.add_development_dependency 'rake'
20
23
  end
@@ -0,0 +1,138 @@
1
+ # Copyright, 2012, by Nick Sieger.
2
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require 'composite_io'
23
+ require 'stringio'
24
+ require 'timeout'
25
+
26
+ RSpec.shared_context "composite io" do
27
+ it "test_full_read_from_several_ios" do
28
+ expect(subject.read).to be == 'the quick brown fox'
29
+ end
30
+
31
+ it "test_partial_read" do
32
+ expect(subject.read(9)).to be == 'the quick'
33
+ end
34
+
35
+ it "test_partial_read_to_boundary" do
36
+ expect(subject.read(10)).to be == 'the quick '
37
+ end
38
+
39
+ it "test_read_with_size_larger_than_available" do
40
+ expect(subject.read(32)).to be == 'the quick brown fox'
41
+ end
42
+
43
+ it "test_read_into_buffer" do
44
+ buf = ''
45
+ subject.read(nil, buf)
46
+ expect(buf).to be == 'the quick brown fox'
47
+ end
48
+
49
+ it "test_multiple_reads" do
50
+ expect(subject.read(4)).to be == 'the '
51
+ expect(subject.read(4)).to be == 'quic'
52
+ expect(subject.read(4)).to be == 'k br'
53
+ expect(subject.read(4)).to be == 'own '
54
+ expect(subject.read(4)).to be == 'fox'
55
+ end
56
+
57
+ it "test_read_after_end" do
58
+ subject.read
59
+ expect(subject.read).to be == ""
60
+ end
61
+
62
+ it "test_read_after_end_with_amount" do
63
+ subject.read(32)
64
+ expect(subject.read(32)).to be_nil
65
+ end
66
+
67
+ it "test_second_full_read_after_rewinding" do
68
+ subject.read
69
+ subject.rewind
70
+ expect(subject.read).to be == 'the quick brown fox'
71
+ end
72
+
73
+ # Was apparently broken on JRuby due to http://jira.codehaus.org/browse/JRUBY-7109
74
+ it "test_compatible_with_copy_stream" do
75
+ target_io = StringIO.new
76
+ Timeout.timeout(1) do # Not sure why we need this in the spec?
77
+ IO.copy_stream(subject, target_io)
78
+ end
79
+ expect(target_io.string).to be == "the quick brown fox"
80
+ end
81
+ end
82
+
83
+ RSpec.describe CompositeReadIO do
84
+ describe "generic io" do
85
+ subject {StringIO.new('the quick brown fox')}
86
+
87
+ include_context "composite io"
88
+ end
89
+
90
+ describe "composite io" do
91
+ subject {CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick '), StringIO.new('brown '), StringIO.new('fox'))}
92
+
93
+ include_context "composite io"
94
+ end
95
+
96
+ describe "nested composite io" do
97
+ subject {CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')), StringIO.new('brown '), StringIO.new('fox'))}
98
+
99
+ include_context "composite io"
100
+ end
101
+
102
+ describe "unicode composite io" do
103
+ let(:utf8_io) {File.open(File.dirname(__FILE__)+'/multibyte.txt')}
104
+ let(:binary_io) {StringIO.new("\x86")}
105
+
106
+ subject {CompositeReadIO.new(binary_io, utf8_io)}
107
+
108
+ it "test_read_from_multibyte" do
109
+ expect(subject.read).to be == "\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n".b
110
+ end
111
+ end
112
+
113
+ it "test_convert_error" do
114
+ expect do
115
+ UploadIO.convert!('tmp.txt', 'text/plain', 'tmp.txt', 'tmp.txt')
116
+ end.to raise_error(ArgumentError, /convert! has been removed/)
117
+ end
118
+
119
+ it "test_empty" do
120
+ expect(subject.read).to be == ""
121
+ end
122
+
123
+ it "test_empty_limited" do
124
+ expect(subject.read(1)).to be_nil
125
+ end
126
+
127
+ it "test_empty_parts" do
128
+ io = CompositeReadIO.new(StringIO.new, StringIO.new('the '), StringIO.new, StringIO.new('quick'))
129
+ expect(io.read(3)).to be == "the"
130
+ expect(io.read(3)).to be == " qu"
131
+ expect(io.read(3)).to be == "ick"
132
+ end
133
+
134
+ it "test_all_empty_parts" do
135
+ io = CompositeReadIO.new(StringIO.new, StringIO.new)
136
+ expect(io.read(1)).to be_nil
137
+ end
138
+ end
File without changes
@@ -0,0 +1,123 @@
1
+ #--
2
+ # Copyright (c) 2007-2013 Nick Sieger.
3
+ # See the file README.txt included with the distribution for
4
+ # software license details.
5
+ #++
6
+
7
+ require 'net/http/post/multipart'
8
+
9
+ RSpec.shared_context "net http multipart" do
10
+ let(:temp_file) {"temp.txt"}
11
+ let(:http_post) do
12
+ Struct.new("HTTPPost", :content_length, :body_stream, :content_type) do
13
+ def set_content_type(type, params = {})
14
+ self.content_type = type + params.map{|k,v|"; #{k}=#{v}"}.join('')
15
+ end
16
+ end
17
+ end
18
+
19
+ after(:each) do
20
+ File.delete(temp_file) rescue nil
21
+ end
22
+
23
+ def assert_results(post)
24
+ expect(post.content_length).to be > 0
25
+ expect(post.body_stream).to_not be_nil
26
+
27
+ expect(post['content-type']).to be == "multipart/form-data; boundary=#{post.boundary}"
28
+
29
+ body = post.body_stream.read
30
+ boundary_regex = Regexp.quote(post.boundary)
31
+
32
+ expect(body).to be =~ /1234567890/
33
+
34
+ # ensure there is at least one boundary
35
+ expect(body).to be =~ /^--#{boundary_regex}\r\n/
36
+
37
+ # ensure there is an epilogue
38
+ expect(body).to be =~ /^--#{boundary_regex}--\r\n/
39
+ expect(body).to be =~ /text\/plain/
40
+
41
+ if (body =~ /multivalueParam/)
42
+ expect(body.scan(/^.*multivalueParam.*$/).size).to be == 2
43
+ end
44
+ end
45
+
46
+ def assert_additional_headers_added(post, parts_headers)
47
+ post.body_stream.rewind
48
+ body = post.body_stream.read
49
+ parts_headers.each do |part, headers|
50
+ headers.each do |k,v|
51
+ expect(body).to be =~ /#{k}: #{v}/
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ RSpec.describe Net::HTTP::Post::Multipart do
58
+ include_context "net http multipart"
59
+
60
+ it "test_form_multipart_body" do
61
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
62
+ @io = File.open(TEMP_FILE)
63
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
64
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
65
+ end
66
+
67
+ it "test_form_multipart_body_with_stringio" do
68
+ @io = StringIO.new("1234567890")
69
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
70
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
71
+ end
72
+
73
+ it "test_form_multiparty_body_with_parts_headers" do
74
+ @io = StringIO.new("1234567890")
75
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
76
+ parts = { :text => 'bar', :file => @io }
77
+ headers = {
78
+ :parts => {
79
+ :text => { "Content-Type" => "part/type" },
80
+ :file => { "Content-Transfer-Encoding" => "part-encoding" }
81
+ }
82
+ }
83
+
84
+ request = Net::HTTP::Post::Multipart.new("/foo/bar", parts, headers)
85
+ assert_results request
86
+ assert_additional_headers_added(request, headers[:parts])
87
+ end
88
+
89
+ it "test_form_multipart_body_with_array_value" do
90
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
91
+ @io = File.open(TEMP_FILE)
92
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
93
+ params = {:foo => ['bar', 'quux'], :file => @io}
94
+ headers = { :parts => {
95
+ :foo => { "Content-Type" => "application/json; charset=UTF-8" } } }
96
+ post = Net::HTTP::Post::Multipart.new("/foo/bar", params, headers)
97
+
98
+ expect(post.content_length).to be > 0
99
+ expect(post.body_stream).to_not be_nil
100
+
101
+ body = post.body_stream.read
102
+ expect(body.lines.grep(/name="foo"/).length).to be == 2
103
+ expect(body).to be =~ /Content-Type: application\/json; charset=UTF-8/
104
+ end
105
+
106
+ it "test_form_multipart_body_with_arrayparam" do
107
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
108
+ @io = File.open(TEMP_FILE)
109
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
110
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :multivalueParam => ['bar','bah'], :file => @io)
111
+ end
112
+ end
113
+
114
+ RSpec.describe Net::HTTP::Put::Multipart do
115
+ include_context "net http multipart"
116
+
117
+ it "test_form_multipart_body_put" do
118
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
119
+ @io = File.open(TEMP_FILE)
120
+ @io = UploadIO.new @io, "text/plain", TEMP_FILE
121
+ assert_results Net::HTTP::Put::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
122
+ end
123
+ end