Upload file using Net::HTTP in Ruby
First, you'll need to know how the browser work on uploading files or how the HTTP request looks like when sending as upload files request. To upload files in the browser, we use a form like this: < form enctype = " multipart/form-data " action = " http://localhost:3000/ " ...
First, you'll need to know how the browser work on uploading files or how the HTTP request looks like when sending as upload files request.
To upload files in the browser, we use a form like this:
<form enctype="multipart/form-data" action="http://localhost:3000/" method="POST"> <input type="hidden" name="MAX_FILE_SIZE" value="100000" /> Choose a file to upload: <input name="uploadedfile" type="file" /><br /> <input type="submit" value="Upload File" /> </form>
The browser will construct a multipart HTTP request message and send it to the webserver:
POST / HTTP/1.1 Host: localhost:3000 Content-Length: 1325 Origin: http://localhost:3000 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L . . . <other headers> ------WebKitFormBoundaryePkpFF7tjBAqx29L Content-Disposition: form-data; name="MAX_FILE_SIZE" 100000 ------WebKitFormBoundaryePkpFF7tjBAqx29L Content-Disposition: form-data; name="uploadedfile"; filename="image.png" Content-Type: image/png <file data (image.png binary)> ------WebKitFormBoundaryePkpFF7tjBAqx29L--
Instead of URL encoding the form parameters, the form parameters (including the file data) are sent as sections in a multipart document in the body of the request.
As you can see in the above example, the multipart HTTP request message consists of a number of sections separated by a boundary=
----WebKitFormBoundaryePkpFF7tjBAqx29L
Each section represent a set value in the form (form field) and it contains a number of headers, a , content (form field values) and finishes with a . So, the general normal field will look like this:
Content-Disposition: form-data; name="form-field" form-field-value
And for the file filed, it will look like this:
Content-Disposition: form-data; name="fieldname"; filename="filename" Content-Type: file-mime-type BINARYDATA...
The request body will be terminated by boundary + "--".
The Net::HTTP ruby library only accepts raw content, which mean it will only accepts a string HTTP request. So, in order to trigger a post upload file request, we must build the request string message manually.
Let's build the multipart sections first. There are two type of sections: one is normal and the other is file part. We will need two method for each section:
Normal section
def multipart_text key, value content = "Content-Disposition: form-data; name="#{key}"" << " " << " " << "#{value}" << " " end
File section
def multipart_file key, filename, mime_type, content content = "Content-Disposition: form-data; name="#{key}"; filename="#{filename}"#{" "}" << "Content-Type: #{mime_type} " << " " << "#{content}" << " " end
These two method is encapsulate in a class called MultipartPost, which I will used to make the multipart request body and issuse a request to a web server. This class will have an array attribute to hold all the sections of the request and an attribute to hold the request url.
class MultipartPost EOL = " " def initialize uri, &block @params = Array.new @uri = URI.parse uri instance_eval &block if block end private def multipart_text key, value content = "Content-Disposition: form-data; name="#{key}"" << EOL << EOL << "#{value}" << EOL end def multipart_file key, filename, mime_type, content content = "Content-Disposition: form-data; name="#{key}"; filename="#{filename}"#{EOL}" << "Content-Type: #{mime_type} " << EOL << "#{content}" << EOL end end
To add sections to the @params, I will need two addition methods:
class MultipartPost # omited codes... def params_part key, value @params << multipart_text(key, value) end def files_part key, filename, mime_type, content @params << multipart_file(key, filename, mime_type, content) end private # omited codes... end
Now, I have to put all the section into one string. Remeber that each section will be separated by a boundary and terminated with boundary + "--". So, this class has to have a method to putting it altogether:
class MultipartPost BOUNDARY = "-----------RubyMultipartPost" # omited codes... def request_body body = @params.map{|p| "--#{BOUNDARY}#{EOL}" << p}.join "" body << "#{EOL}--#{BOUNDARY}--#{EOL}" end # omited codes... end
Now, I have everything I need to create a multipart post request string for upload files. The only left is issue the request to a web server. This is the job for ruby Net:HTTP library. The MultipartPost class need a method that use Net::HTTP library to issuse the request, like so:
def run http = Net::HTTP.new @uri.host, @uri.port request = Net::HTTP::Post.new @uri.request_uri request.body = request_body request.set_content_type "multipart/form-data", {"boundary" => BOUNDARY} res = http.request request res.body end
Using this class is very simple. All you need to do is create a new MultipartPost class instance with an url string and passed it a block that constructing the section, and then call run() method on that instance.
multi_part = MultipartPost.new post_url do params_part "key", value files_part "file-key", filename, file_content_type, file-data end multi_part.run
My full source code here