Upload a file via POST with Net::HTTP
Posted by Peter Donald Thu, 02 Mar 2006 02:57:00 GMT
To upload a file to a website I needed to supply the data with a content type of “multipart/form-data”. The Net:HTTP API does not offer any such functionality, it just accepts raw content data. So I needed to roll my own.
The “multipart/form-data” content type consists of a number of secions separated by --BOUNDARY\r\n and terminated by BOUNDARY--\r\n where BOUNDARY is a string that does not appear in the content of any of the data transmitted to the server.
Each section represents a form field and contains a number of headers, a \r\n, the content and finishes with a \r\n. Normal form fields look like
Content-Disposition: form-data; name="mykey"
mydatawhile file fields must include a few more headers.
Content-Disposition: form-data; name="mykey"; filename="filename"
Content-Transfer-Encoding: binary
Content-Type: text/plain
DATADATADATADATADATADATADATA...To construct the parameters in ruby I use the following code;
def text_to_multipart(key,value)
return "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"\r\n" +
"\r\n" +
"#{value}\r\n"
end
def file_to_multipart(key,filename,mime_type,content)
return "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{filename}\"\r\n" +
"Content-Transfer-Encoding: binary\r\n" +
"Content-Type: #{mime_type}\r\n" +
"\r\n" +
"#{content}\r\n"
endTo put it all together you need to join the parameters with boundary separators between each section. This can be done via
boundary = '349832898984244898448024464570528145'
query =
params.collect {|p| '--' + boundary + "\r\n" + p}.join('') + "--" + boundary + "--\r\n"The last thing that needs to be done is to make sure that you set the HTTP Header Content-type to multipart/form-data; boundary=BOUNDARY.
A complete example that I extracted from code that uploads a css file to the w3c validator service is as follows.
params = [
file_to_multipart('file','file.css','text/css',data),
text_to_multipart('warning','1'),
text_to_multipart('profile','css2'),
text_to_multipart('usermedium','all') ]
boundary = '349832898984244898448024464570528145'
query =
params.collect {|p| '--' + boundary + "\r\n" + p}.join('') + "--" + boundary + "--\r\n"
response = http.start('jigsaw.w3.org').
post2("/css-validator/validator",
query,
"Content-type" => "multipart/form-data; boundary=" + boundary)It was a little bit painful to figure out “multipart/form-data” via Ethereal but relatively easy to implement. Hope this helps!
Update 3rd of October, 2006:
Slight correction supplied by Andrew Willis so that last boundary is '--' + boundary + '--' rather than just boundary + '--'.