jwagener-multipart-post 1.0.3

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,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,62 @@
1
+ = multipart-post
2
+
3
+ * http://github.com/nicksieger/multipart-post
4
+
5
+ == DESCRIPTION:
6
+
7
+ Adds a streamy multipart form post capability to Net::HTTP. Also
8
+ supports other methods besides POST.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * Appears to actually work. A good feature to have.
13
+ * Encapsulates posting of file/binary parts and name/value parameter parts, similar to
14
+ most browsers' file upload forms.
15
+ * Provides an UploadIO helper module to prepare IO objects for inclusion in the params
16
+ hash of the multipart post object.
17
+
18
+ == SYNOPSIS:
19
+
20
+ require 'net/http/post/multipart'
21
+
22
+ url = URI.parse('http://www.example.com/upload')
23
+ File.open("./image.jpg") do |jpg|
24
+ req = Net::HTTP::Post::Multipart.new url.path,
25
+ "file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
26
+ res = Net::HTTP.start(url.host, url.port) do |http|
27
+ http.request(req)
28
+ end
29
+ end
30
+
31
+ == REQUIREMENTS:
32
+
33
+ None
34
+
35
+ == INSTALL:
36
+
37
+ gem install multipart-post
38
+
39
+ == LICENSE:
40
+
41
+ (The MIT License)
42
+
43
+ Copyright (c) 2007-2010 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,24 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'hoe'
4
+ require 'lib/multipart_post'
5
+
6
+ Hoe.plugin :gemcutter
7
+ hoe = Hoe.spec("multipart-post") do |p|
8
+ p.version = MultipartPost::VERSION
9
+ p.rubyforge_name = "caldersphere"
10
+ p.author = "Nick Sieger"
11
+ p.url = "http://github.com/nicksieger/multipart-post"
12
+ p.email = "nick@nicksieger.com"
13
+ 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."
14
+ p.summary = "Creates a multipart form post accessory for Net::HTTP."
15
+ end
16
+ hoe.spec.dependencies.delete_if { |dep| dep.name == "hoe" }
17
+
18
+ task :gemspec do
19
+ File.open("#{hoe.name}.gemspec", "w") {|f| f << hoe.spec.to_ruby }
20
+ end
21
+ task :package => :gemspec
22
+ rescue LoadError
23
+ puts "You really need Hoe installed to be able to package this gem"
24
+ end
@@ -0,0 +1,94 @@
1
+ #--
2
+ # (c) Copyright 2007-2010 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
+ local_path = ""
65
+ if io.respond_to? :read
66
+ local_path = filename_or_io.path
67
+ else
68
+ io = File.open(filename_or_io)
69
+ local_path = filename_or_io
70
+ end
71
+ filename ||= local_path
72
+
73
+ convert!(io, content_type, File.basename(filename), local_path)
74
+ io
75
+ end
76
+
77
+ # Enhance an existing IO for including in the params hash of a
78
+ # Net::HTTP::Post::Multipart by adding #content_type, #original_filename,
79
+ # and #local_path methods to the object's singleton class.
80
+ def self.convert!(io, content_type, original_filename, local_path)
81
+ io.instance_eval(<<-EOS, __FILE__, __LINE__)
82
+ def content_type
83
+ "#{content_type}"
84
+ end
85
+ def original_filename
86
+ "#{original_filename}"
87
+ end
88
+ def local_path
89
+ "#{local_path}"
90
+ end
91
+ EOS
92
+ io
93
+ end
94
+ 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,67 @@
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 = StringIO.new(build_head(boundary, name, io.original_filename, io.content_type, file_length))
43
+ @foot = StringIO.new("\r\n")
44
+ @length = @head.length + file_length + @foot.length
45
+ @io = CompositeReadIO.new(@head, io, @foot)
46
+ end
47
+
48
+ def build_head(boundary, name, filename, type, content_len)
49
+ part = ''
50
+ part << "--#{boundary}\r\n"
51
+ part << "Content-Disposition: form-data; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n"
52
+ part << "Content-Length: #{content_len}\r\n"
53
+ part << "Content-Type: #{type}\r\n"
54
+ part << "Content-Transfer-Encoding: binary\r\n"
55
+ part << "\r\n"
56
+ end
57
+ end
58
+
59
+ # Represents the epilogue or closing boundary.
60
+ class EpiloguePart
61
+ include Part
62
+ def initialize(boundary)
63
+ @part = "--#{boundary}--\r\n"
64
+ @io = StringIO.new(@part)
65
+ end
66
+ end
67
+ 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,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwagener-multipart-post
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 3
10
+ version: 1.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Nick Sieger
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-04-27 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rubyforge
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 2
32
+ - 0
33
+ - 4
34
+ version: 2.0.4
35
+ type: :development
36
+ version_requirements: *id001
37
+ 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."
38
+ email: nick@nicksieger.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - Manifest.txt
45
+ - README.txt
46
+ files:
47
+ - lib/composite_io.rb
48
+ - lib/multipartable.rb
49
+ - lib/parts.rb
50
+ - lib/net/http/post/multipart.rb
51
+ - Manifest.txt
52
+ - Rakefile
53
+ - README.txt
54
+ - test/test_composite_io.rb
55
+ - test/net/http/post/test_multipart.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/nicksieger/multipart-post
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --main
63
+ - README.txt
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project: caldersphere
87
+ rubygems_version: 1.3.7
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Creates a multipart form post accessory for Net::HTTP.
91
+ test_files:
92
+ - test/net/http/post/test_multipart.rb
93
+ - test/test_composite_io.rb