Wednesday, February 10, 2010

Multipart form data post through ruby script

Well its been long I blogged but better late than never... :)))
Been busy with projects, visit to India etc etc ... I can come up with a long list of excuses but none of them will be procrastinating ... hehehe

I forced my self out of hibernation to blog about a recent (strange ??) problem I faced.
In one of my projects, I came across a fairly common requirement to upload a file (multipart form-data post) through a ruby script. Pretty simple huhhh!

Unfortunately I was stumped to find that net/http library does not support it (still dont believe it!!). The documentation is very poor and it took me sometime to figure out net/http treats them all as string. CRAP !!!.

Some nice libraries eg: rest_client, httpclient, curb etc.. do a decent job in terms of posting the file, but they all mess with either creating incorrect boundaries for multi-part form or mess with content_length header of form-post

My requirement was to make http post call (multipart form data post) to Domino server. The domino app was very particular about content_length header, which is where I faced the biggest hurdle in using them.

Left with no choice, rolled up my sweet multipart form post.

Sharing with you all ruby-multipart-post.

How to use ==> here

Any feedback, most welcome ...

Enjoy !!!

15 comments:

Rebecca said...

This looks pretty sweet!

When I tried it though, I got the following error:

/Library/Ruby/Gems/1.8/gems/ruby-multipart-post-0.1.0/lib/stream.rb:12:in `+': String can't be coerced into Fixnum (TypeError)
from /Library/Ruby/Gems/1.8/gems/ruby-multipart-post-0.1.0/lib/stream.rb:12:in `size'
from /Library/Ruby/Gems/1.8/gems/ruby-multipart-post-0.1.0/lib/stream.rb:11:in `each'
from /Library/Ruby/Gems/1.8/gems/ruby-multipart-post-0.1.0/lib/stream.rb:11:in `size'
from /Library/Ruby/Gems/1.8/gems/ruby-multipart-post-0.1.0/lib/multi_part_post.rb:38:in `submit'
from post_file.rb:11




My script:

require 'rubygems'
require "ruby-multipart-post"

params = {
"appKey" => "myAppKey", "userToken" => "myUserToken", "folder" => "myFolder",
"cpFile1" => FileUploadIO.new("/path/to/my/image.jpg", "image/jpeg")
}

url = 'http://upload.cafepress.com/image.upload.cp'
multipart_post = MultiPart::Post.new(params)
multipart_post.submit(url)

Any idea what the problem might be?
Thanks!
Rebecca

Amit Kumar said...

Hey thanks Rebecca for posting the comment. I have fixed the issue. Please update the version of gem and try again. It should work now....

Mark Bykerk Kauffman said...

Hi Amit, Thank you for this blog and for the ruby-multipart-post gem. I'd like to get it to work with https. The following made the connection to https://mytestserver.edu/someapp/somescript with the httpclient, but of course I couldn't do a multipart:
client = HTTPClient.new
client.ssl_config.ciphers = 'ALL'
client.post(post_url, params)

Any suggestions for getting an https connection with ruby-multipart-post? Thanks.

Anonymous said...

Hi Amit, I'am Jenny.
I got some probleme with multipart-posting. I store my data on DB, i got a message :

'--!ruby/object:ActionController::UploadedStringIO content_type: multipart/form-data; boundary= original_filename: myfile original_path: myfile '

My code:

myfile = File.new('/home/harks/Programation/Rails/ParkeonScriptV5/park/0000200368.parkeon')
path_up = myfile.path

params = {"upload[name]" => "TestConsole",
"upload[datafile]" => FileUploadIO.new(path_up,'multipart/form-data; boundary=')
}

url = 'http://localhost:3000/uploads'
current_post = MultiPart::Post.new(params)
query = '/new'

current_post.submit(url,query)


Thanks for listening

Amit Kumar said...

Hi Anonymous,
The content passed to the FileUploadIO is incorrect, you need to specify the content type of the file eg: "image/jpeg".
Sorry, if the wiki does not specify it clearly. I will update the description.

Amit Kumar said...

Hi Mark,

added the support for https.
Re-install and you should be good to go.
Let me know in case you find any issues

Bloglarim said...

Rss
Feed Suggest
- Suggest
Blog
- Not-Reciprocal
Submission
- Submission
Express
- Auto
Submission
- Blog
Ping
- Submission
Tag

Abhi said...

Hey Amit,

I had to make a few changes to get this working for me. Here is the diff. Let me know if this makes sense.


*** multi_part_post.rb.org 2011-03-22 08:49:53.000000000 -0700
--- multi_part_post.rb 2011-03-22 20:30:58.000000000 -0700
***************
*** 33,45 ****
def submit(to_url, query_string=nil)
post_stream = Stream::MultiPart.new(@parts)
url = URI.parse( to_url )
! post_url_with_query_string = "#{url.path}?#{query_string}"
req = Net::HTTP::Post.new(post_url_with_query_string, @request_headers)
req.content_length = post_stream.size
req.content_type = 'multipart/form-data; boundary=' + multi_part_boundary
req.body_stream = post_stream
! req.use_ssl = true if url.scheme == "https"
! res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }

#close all the open hooks to the file on file-system
@streams.each do |local_stream|
--- 33,49 ----
def submit(to_url, query_string=nil)
post_stream = Stream::MultiPart.new(@parts)
url = URI.parse( to_url )
! post_url_with_query_string = "#{url.path}"
! unless query_string.nil?
! post_url_with_query_string = post_url_with_query_string + "?#{query_string}"
! end
req = Net::HTTP::Post.new(post_url_with_query_string, @request_headers)
req.content_length = post_stream.size
req.content_type = 'multipart/form-data; boundary=' + multi_part_boundary
req.body_stream = post_stream
! http_handle = Net::HTTP.new(url.host, url.port)
! http_handle.use_ssl = true if url.scheme == "https"
! res = http_handle.start {|http| http.request(req)}

#close all the open hooks to the file on file-system
@streams.each do |local_stream|

Amit Kumar said...

@abhi Thanks for the suggestion. Making change in the gem.

Abhi said...

Hey Amit,

I had to make another minor modification to get this work for me.
The diff is shown below. I had to remove the first "\r\n" on line 26. They are probably unnecessary.
Let me know if this makes sense.


*** multi_part_post.rb.org 2011-03-25 06:56:37.000000000 +0000
--- multi_part_post.rb 2011-03-25 06:57:35.000000000 +0000
***************
*** 23,29 ****
@parts << Parts::StringParam.new("--#{multi_part_boundary}\r\n" + "Content-Disposition: form-data; name=\"#{key}\"\r\n" + "\r\n" + "#{val}\r\n")
end
end
! @parts << Parts::StringParam.new( "\r\n--" + multi_part_boundary + "--\r\n" )
end

def multi_part_boundary
--- 23,29 ----
@parts << Parts::StringParam.new("--#{multi_part_boundary}\r\n" + "Content-Disposition: form-data; name=\"#{key}\"\r\n" + "\r\n" + "#{val}\r\n")
end
end
! @parts << Parts::StringParam.new( "--" + multi_part_boundary + "--\r\n" )
end

def multi_part_boundary

Abhi said...

Hey Amit,

Just wondering if the last changes I made make sense? Would you consider updating the gem?

-Abhi

Amit Kumar said...

strangely that has been working for me
do you get error?
can you post stack trace ?

Abhi said...
This comment has been removed by the author.
Abhi said...

Actually I don't see an error message. I was using ruby-multipart-post to post a form to a legacy web application. My form had only text fields and no files. I observed that the web app would consistently reject the last form element because it had an extraneous "CR LF" appended to value of the last from element. I researched a little and found that lines of data in a multipart form are separated by a CR LF. With ruby-multipart-post there seems to an excess "CR LF" sequence after the last text field value and before the form boundary. I can send you some of my application logs and ethereal traces of a working and non-working post. The working post was done from a browser and the non working post using the gem. I'll do this later in the day or sometime tomorrow.
But do you think there could be some sense in my theory?

-Abhi

Amit Kumar said...

Thanks for the detailed analysis. It would be interesting to know if "CR LF" would drop the submitted form data. I would try to replicate this. In the meantime, please send me some log statements and means to replicate it. This would save me some extra hours.

Thanks!