multipart-post 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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