nicksieger-multipart-post 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ lib/composite_io.rb
2
+ lib/multipartable.rb
3
+ lib/parts.rb
4
+ lib/net/http/post/multipart.rb
5
+ Manifest.txt
6
+ Rakefile
7
+ README.txt
8
+ test/test_composite_io.rb
9
+ test/net/http/post/test_multipart.rb
@@ -0,0 +1,61 @@
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
+ gem install multipart-post
37
+
38
+ == LICENSE:
39
+
40
+ (The MIT License)
41
+
42
+ Copyright (c) 2007-2009 Nick Sieger <nick@nicksieger.com>
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining
45
+ a copy of this software and associated documentation files (the
46
+ 'Software'), to deal in the Software without restriction, including
47
+ without limitation the rights to use, copy, modify, merge, publish,
48
+ distribute, sublicense, and/or sell copies of the Software, and to
49
+ permit persons to whom the Software is furnished to do so, subject to
50
+ the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be
53
+ included in all copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
59
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
61
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'hoe'
4
+
5
+ require 'lib/multipart_post'
6
+
7
+ hoe = Hoe.new("multipart-post", MultipartPost::VERSION) do |p|
8
+ p.rubyforge_name = "caldersphere"
9
+ p.author = "Nick Sieger"
10
+ p.url = "http://github.com/nicksieger/multipart-post"
11
+ p.email = "nick@nicksieger.com"
12
+ 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."
13
+ p.summary = "Creates a multipart form post accessory for Net::HTTP."
14
+ end
15
+
16
+ task :gemspec do
17
+ File.open("#{hoe.name}.gemspec", "w") {|f| f << hoe.spec.to_ruby }
18
+ end
19
+ rescue LoadError
20
+ puts "You really need Hoe installed to be able to package this gem"
21
+ 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,13 @@
1
+ require 'parts'
2
+ module Multipartable
3
+ DEFAULT_BOUNDARY = "-----------RubyMultipartPost"
4
+ def initialize(path, params, headers={}, boundary = DEFAULT_BOUNDARY)
5
+ super(path, headers)
6
+ parts = params.map {|k,v| Parts::Part.new(boundary, k, v)}
7
+ parts << Parts::EpiloguePart.new(boundary)
8
+ ios = parts.map{|p| p.to_io }
9
+ self.set_content_type("multipart/form-data", { "boundary" => boundary })
10
+ self.content_length = parts.inject(0) {|sum,i| sum + i.length }
11
+ self.body_stream = CompositeReadIO.new(*ios)
12
+ end
13
+ end
@@ -0,0 +1,27 @@
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
+ require 'multipartable'
12
+ require 'parts'
13
+
14
+ module Net #:nodoc:
15
+ class HTTP #:nodoc:
16
+ class Put
17
+ class Multipart < Put
18
+ include Multipartable
19
+ end
20
+ end
21
+ class Post #:nodoc:
22
+ class Multipart < Post
23
+ include Multipartable
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ module Parts
2
+ module Part #:nodoc:
3
+ def self.new(boundary, name, value)
4
+ if value.respond_to? :content_type
5
+ FilePart.new(boundary, name, value)
6
+ else
7
+ ParamPart.new(boundary, name, value)
8
+ end
9
+ end
10
+
11
+ def length
12
+ @part.length
13
+ end
14
+
15
+ def to_io
16
+ @io
17
+ end
18
+ end
19
+
20
+ class ParamPart
21
+ include Part
22
+ def initialize(boundary, name, value)
23
+ @part = build_part(boundary, name, value)
24
+ @io = StringIO.new(@part)
25
+ end
26
+
27
+ def build_part(boundary, name, value)
28
+ part = ''
29
+ part << "--#{boundary}\r\n"
30
+ part << "Content-Disposition: form-data; name=\"#{name.to_s}\"\r\n"
31
+ part << "\r\n"
32
+ part << "#{value}\r\n"
33
+ end
34
+ end
35
+
36
+ # Represents a part to be filled from file IO.
37
+ class FilePart
38
+ include Part
39
+ attr_reader :length
40
+ def initialize(boundary, name, io)
41
+ file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path)
42
+ @head = build_head(boundary, name, io.original_filename, io.content_type, file_length)
43
+ @length = @head.length + file_length
44
+ @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new("\r\n"))
45
+ end
46
+
47
+ def build_head(boundary, name, filename, type, content_len)
48
+ part = ''
49
+ part << "--#{boundary}\r\n"
50
+ part << "Content-Disposition: form-data; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n"
51
+ part << "Content-Length: #{content_len}\r\n"
52
+ part << "Content-Type: #{type}\r\n"
53
+ part << "Content-Transfer-Encoding: binary\r\n"
54
+ part << "\r\n"
55
+ end
56
+ end
57
+
58
+ # Represents the epilogue or closing boundary.
59
+ class EpiloguePart
60
+ include Part
61
+ def initialize(boundary)
62
+ @part = "--#{boundary}--\r\n"
63
+ @io = StringIO.new(@part)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
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
+ def test_form_multipart_body_put
30
+ File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
31
+ @io = File.open(TEMP_FILE)
32
+ UploadIO.convert! @io, "text/plain", TEMP_FILE, TEMP_FILE
33
+ assert_results Net::HTTP::Put::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
34
+ end
35
+
36
+ def test_form_multipart_body_with_stringio
37
+ @io = StringIO.new("1234567890")
38
+ UploadIO.convert! @io, "text/plain", TEMP_FILE, TEMP_FILE
39
+ assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
40
+ end
41
+
42
+ def assert_results(post)
43
+ assert post.content_length && post.content_length > 0
44
+ assert post.body_stream
45
+ assert_equal "multipart/form-data; boundary=#{Multipartable::DEFAULT_BOUNDARY}", post['content-type']
46
+ body = post.body_stream.read
47
+ boundary_regex = Regexp.quote Multipartable::DEFAULT_BOUNDARY
48
+ assert body =~ /1234567890/
49
+ # ensure there is at least one boundary
50
+ assert body =~ /^--#{boundary_regex}\r\n/
51
+ # ensure there is an epilogue
52
+ assert body =~ /^--#{boundary_regex}--\r\n/
53
+ assert body =~ /text\/plain/
54
+ end
55
+ 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,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nicksieger-multipart-post
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.9"
5
+ platform: ruby
6
+ authors:
7
+ - Nick Sieger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.8.2
23
+ version:
24
+ 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."
25
+ email: nick@nicksieger.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - Manifest.txt
32
+ - README.txt
33
+ files:
34
+ - lib/composite_io.rb
35
+ - lib/multipartable.rb
36
+ - lib/parts.rb
37
+ - lib/net/http/post/multipart.rb
38
+ - Manifest.txt
39
+ - Rakefile
40
+ - README.txt
41
+ - test/test_composite_io.rb
42
+ - test/net/http/post/test_multipart.rb
43
+ has_rdoc: true
44
+ homepage: http://github.com/nicksieger/multipart-post
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project: caldersphere
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: Creates a multipart form post accessory for Net::HTTP.
70
+ test_files:
71
+ - test/net/http/post/test_multipart.rb
72
+ - test/test_composite_io.rb