jwagener-multipart-post 1.0.3

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,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