multipart-post 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ lib/composite_io.rb
2
+ lib/net/http/post/multipart.rb
3
+ Manifest.txt
4
+ Rakefile
5
+ README.txt
6
+ test/test_composite_io.rb
7
+ test/net/http/post/test_multipart.rb
@@ -0,0 +1,62 @@
1
+ = multipart-post
2
+
3
+ * http://github.com/nicksieger/multipart-post
4
+
5
+ == DESCRIPTION:
6
+
7
+ Adds a multipart form post capability to Net::HTTP.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Appears to actually work. A good feature to have.
12
+ * Encapsulates posting of file/binary parts and name/value parameter parts, similar to
13
+ most browsers' file upload forms.
14
+ * Provides an UploadIO helper module to prepare IO objects for inclusion in the params
15
+ hash of the multipart post object.
16
+
17
+ == SYNOPSIS:
18
+
19
+ require 'net/http/post/multipart'
20
+
21
+ url = URI.parse('http://www.example.com/upload')
22
+ File.open("./image.jpg") do |jpg|
23
+ req = Net::HTTP::Post::Multipart.new url.path,
24
+ "file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
25
+ res = Net::HTTP.start(url.host, url.port) do |http|
26
+ http.request(req)
27
+ end
28
+ end
29
+
30
+ == REQUIREMENTS:
31
+
32
+ None
33
+
34
+ == INSTALL:
35
+
36
+ rake package
37
+ gem install pkg/multipart-form*.gem
38
+
39
+ == LICENSE:
40
+
41
+ (The MIT License)
42
+
43
+ Copyright (c) 2007-2008 Nick Sieger <nick@nicksieger.com>
44
+
45
+ Permission is hereby granted, free of charge, to any person obtaining
46
+ a copy of this software and associated documentation files (the
47
+ 'Software'), to deal in the Software without restriction, including
48
+ without limitation the rights to use, copy, modify, merge, publish,
49
+ distribute, sublicense, and/or sell copies of the Software, and to
50
+ permit persons to whom the Software is furnished to do so, subject to
51
+ the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be
54
+ included in all copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
57
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
58
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
59
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
60
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
61
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
62
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'hoe'
4
+
5
+ hoe = Hoe.new("multipart-post", '0.1') do |p|
6
+ p.rubyforge_name = "caldersphere"
7
+ p.author = "Nick Sieger"
8
+ p.url = "http://github.com/nicksieger/multipart-post"
9
+ p.email = "nick@nicksieger.com"
10
+ p.description = "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."
11
+ p.summary = "Creates a multipart form post accessory for Net::HTTP."
12
+ end
13
+
14
+ task :gemspec do
15
+ File.open("#{hoe.name}.gemspec", "w") {|f| f << hoe.spec.to_ruby }
16
+ end
17
+ rescue LoadError
18
+ puts "You really need Hoe installed to be able to package this gem"
19
+ end
@@ -0,0 +1,89 @@
1
+ #--
2
+ # (c) Copyright 2007-2008 Nick Sieger.
3
+ # See the file README.txt included with the distribution for
4
+ # software license details.
5
+ #++
6
+
7
+ # Concatenate together multiple IO objects into a single, composite IO object
8
+ # for purposes of reading as a single stream.
9
+ #
10
+ # Usage:
11
+ #
12
+ # crio = CompositeReadIO.new(StringIO.new('one'), StringIO.new('two'), StringIO.new('three'))
13
+ # puts crio.read # => "onetwothree"
14
+ #
15
+ class CompositeReadIO
16
+ # Create a new composite-read IO from the arguments, all of which should
17
+ # respond to #read in a manner consistent with IO.
18
+ def initialize(*ios)
19
+ @ios = ios.flatten
20
+ end
21
+
22
+ # Read from the IO object, overlapping across underlying streams as necessary.
23
+ def read(amount = nil, buf = nil)
24
+ buffer = buf || ''
25
+ done = if amount; nil; else ''; end
26
+ partial_amount = amount
27
+
28
+ loop do
29
+ result = done
30
+
31
+ while !@ios.empty? && (result = @ios.first.read(partial_amount)) == done
32
+ @ios.shift
33
+ end
34
+
35
+ buffer << result if result
36
+ partial_amount -= result.length if partial_amount && result != done
37
+
38
+ break if partial_amount && partial_amount <= 0
39
+ break if result == done
40
+ end
41
+
42
+ if buffer.length > 0
43
+ buffer
44
+ else
45
+ done
46
+ end
47
+ end
48
+ end
49
+
50
+ # Convenience methods for dealing with files and IO that are to be uploaded.
51
+ module UploadIO
52
+ # Create an upload IO suitable for including in the params hash of a
53
+ # Net::HTTP::Post::Multipart.
54
+ #
55
+ # Can take two forms. The first accepts a filename and content type, and
56
+ # opens the file for reading (to be closed by finalizer). The second accepts
57
+ # an already-open IO, but also requires a third argument, the filename from
58
+ # which it was opened.
59
+ #
60
+ # UploadIO.new("file.txt", "text/plain")
61
+ # UploadIO.new(file_io, "text/plain", "file.txt")
62
+ def self.new(filename_or_io, content_type, filename = nil)
63
+ io = filename_or_io
64
+ unless io.respond_to? :read
65
+ io = File.open(filename_or_io)
66
+ filename = filename_or_io
67
+ end
68
+ convert!(io, content_type, File.basename(filename), filename)
69
+ io
70
+ end
71
+
72
+ # Enhance an existing IO for including in the params hash of a
73
+ # Net::HTTP::Post::Multipart by adding #content_type, #original_filename,
74
+ # and #local_path methods to the object's singleton class.
75
+ def self.convert!(io, content_type, original_filename, local_path)
76
+ io.instance_eval(<<-EOS, __FILE__, __LINE__)
77
+ def content_type
78
+ "#{content_type}"
79
+ end
80
+ def original_filename
81
+ "#{original_filename}"
82
+ end
83
+ def local_path
84
+ "#{local_path}"
85
+ end
86
+ EOS
87
+ io
88
+ end
89
+ end
@@ -0,0 +1,102 @@
1
+ #--
2
+ # (c) Copyright 2007-2008 Nick Sieger.
3
+ # See the file README.txt included with the distribution for
4
+ # software license details.
5
+ #++
6
+
7
+ require 'net/http'
8
+ require 'stringio'
9
+ require 'cgi'
10
+ require 'composite_io'
11
+
12
+ module Net #:nodoc:
13
+ class HTTP #:nodoc:
14
+ class Post #:nodoc:
15
+ module Part #:nodoc:
16
+ def self.new(boundary, name, value)
17
+ if value.respond_to? :content_type
18
+ FilePart.new(boundary, name, value)
19
+ else
20
+ ParamPart.new(boundary, name, value)
21
+ end
22
+ end
23
+
24
+ def length
25
+ @part.length
26
+ end
27
+
28
+ def to_io
29
+ @io
30
+ end
31
+ end
32
+
33
+ # Represents a part to be filled with a string name/value pair.
34
+ class ParamPart
35
+ include Part
36
+ def initialize(boundary, name, value)
37
+ @part = build_part(boundary, name, value)
38
+ @io = StringIO.new(@part)
39
+ end
40
+
41
+ def build_part(boundary, name, value)
42
+ part = ''
43
+ part << "--#{boundary}\r\n"
44
+ part << "Content-Disposition: form-data; name=\"#{name.to_s}\"\r\n"
45
+ part << "\r\n"
46
+ part << "#{value}\r\n"
47
+ end
48
+ end
49
+
50
+ # Represents a part to be filled from file IO.
51
+ class FilePart
52
+ include Part
53
+ attr_reader :length
54
+ def initialize(boundary, name, io)
55
+ @head = build_head(boundary, name, io.original_filename, io.content_type)
56
+ file_length = if io.respond_to? :length
57
+ io.length
58
+ else
59
+ File.size(io.local_path)
60
+ end
61
+ @length = @head.length + file_length
62
+ @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new("\r\n"))
63
+ end
64
+
65
+ def build_head(boundary, name, filename, type)
66
+ part = ''
67
+ part << "--#{boundary}\r\n"
68
+ part << "Content-Disposition: form-data; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n"
69
+ part << "Content-Type: #{type}\r\n"
70
+ part << "Content-Transfer-Encoding: binary\r\n"
71
+ part << "\r\n"
72
+ end
73
+ end
74
+
75
+ # Represents the epilogue or closing boundary.
76
+ class EpiloguePart
77
+ include Part
78
+ def initialize(boundary)
79
+ @part = "--#{boundary}--\r\n"
80
+ @io = StringIO.new(@part)
81
+ end
82
+ end
83
+
84
+ DEFAULT_BOUNDARY = "-----------RubyMultipartPost"
85
+
86
+ # Extension to the Net::HTTP::Post class that builds a post body
87
+ # consisting of a multipart mime stream based on the parameters given.
88
+ # See README.txt for synopsis and details.
89
+ class Multipart < Post
90
+ def initialize(path, params, boundary = DEFAULT_BOUNDARY)
91
+ super(path)
92
+ parts = params.map {|k,v| Part.new(boundary, k, v)}
93
+ parts << EpiloguePart.new(boundary)
94
+ ios = parts.map{|p| p.to_io }
95
+ self.set_content_type("multipart/form-data", { "boundary" => boundary })
96
+ self.content_length = parts.inject(0) {|sum,i| sum + i.length }
97
+ self.body_stream = CompositeReadIO.new(*ios)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,49 @@
1
+ #--
2
+ # (c) Copyright 2007-2008 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
+ class Net::HTTP::Post::MultiPartTest < Test::Unit::TestCase
10
+ TEMP_FILE = "temp.txt"
11
+
12
+ HTTPPost = Struct.new("HTTPPost", :content_length, :body_stream, :content_type)
13
+ HTTPPost.module_eval do
14
+ def set_content_type(type, params = {})
15
+ self.content_type = type + params.map{|k,v|"; #{k}=#{v}"}.join('')
16
+ end
17
+ end
18
+
19
+ def teardown
20
+ File.delete(TEMP_FILE) rescue nil
21
+ end
22
+
23
+ def test_form_multipart_body
24
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
25
+ @io = File.open(TEMP_FILE)
26
+ UploadIO.convert! @io, "text/plain", TEMP_FILE, TEMP_FILE
27
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
28
+ end
29
+
30
+ def test_form_multipart_body_with_stringio
31
+ @io = StringIO.new("1234567890")
32
+ UploadIO.convert! @io, "text/plain", TEMP_FILE, TEMP_FILE
33
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
34
+ end
35
+
36
+ def assert_results(post)
37
+ assert post.content_length && post.content_length > 0
38
+ assert post.body_stream
39
+ assert_equal "multipart/form-data; boundary=#{Net::HTTP::Post::DEFAULT_BOUNDARY}", post['content-type']
40
+ body = post.body_stream.read
41
+ boundary_regex = Regexp.quote Net::HTTP::Post::DEFAULT_BOUNDARY
42
+ assert body =~ /1234567890/
43
+ # ensure there is at least one boundary
44
+ assert body =~ /^--#{boundary_regex}\r\n/
45
+ # ensure there is an epilogue
46
+ assert body =~ /^--#{boundary_regex}--\r\n/
47
+ assert body =~ /text\/plain/
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ require 'composite_io'
2
+ require 'stringio'
3
+ require 'test/unit'
4
+
5
+ class CompositeReadIOTest < Test::Unit::TestCase
6
+ def setup
7
+ @io = CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')),
8
+ StringIO.new('brown '), StringIO.new('fox'))
9
+ end
10
+
11
+ def test_full_read_from_several_ios
12
+ assert_equal 'the quick brown fox', @io.read
13
+ end
14
+
15
+ def test_partial_read
16
+ assert_equal 'the quick', @io.read(9)
17
+ end
18
+
19
+ def test_partial_read_to_boundary
20
+ assert_equal 'the quick ', @io.read(10)
21
+ end
22
+
23
+ def test_read_with_size_larger_than_available
24
+ assert_equal 'the quick brown fox', @io.read(32)
25
+ end
26
+
27
+ def test_read_into_buffer
28
+ buf = ''
29
+ @io.read(nil, buf)
30
+ assert_equal 'the quick brown fox', buf
31
+ end
32
+
33
+ def test_multiple_reads
34
+ assert_equal 'the ', @io.read(4)
35
+ assert_equal 'quic', @io.read(4)
36
+ assert_equal 'k br', @io.read(4)
37
+ assert_equal 'own ', @io.read(4)
38
+ assert_equal 'fox', @io.read(4)
39
+ end
40
+
41
+ def test_read_after_end
42
+ @io.read
43
+ assert_equal "", @io.read
44
+ end
45
+
46
+ def test_read_after_end_with_amount
47
+ @io.read(32)
48
+ assert_equal nil, @io.read(32)
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multipart-post
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Nick Sieger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-13 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.5.3
24
+ version:
25
+ description: "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."
26
+ email: nick@nicksieger.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - Manifest.txt
33
+ - README.txt
34
+ files:
35
+ - lib/composite_io.rb
36
+ - lib/net/http/post/multipart.rb
37
+ - Manifest.txt
38
+ - Rakefile
39
+ - README.txt
40
+ - test/test_composite_io.rb
41
+ - test/net/http/post/test_multipart.rb
42
+ has_rdoc: true
43
+ homepage: http://github.com/nicksieger/multipart-post
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --main
47
+ - README.txt
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project: caldersphere
65
+ rubygems_version: 1.2.0
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: Creates a multipart form post accessory for Net::HTTP.
69
+ test_files:
70
+ - test/net/http/post/test_multipart.rb
71
+ - test/test_composite_io.rb