On Jan 23, 2007, at 12:09 AM, Zed A. Shaw wrote:
On Mon, 22 Jan 2007 19:27:35 -0600 [EMAIL PROTECTED] wrote:By default, Mongrel will delete the HTTP request body and short circuit calling any handlers if a request is interrupted or incomplete. Unfortunately, this breaks any attempt to correctly handle a partial PUT. (BTW, PUT is *way* more efficient for uploads compared to POST which requires a MIME parsing step.) So, about a month ago I wrote up some patches to Mongrel 0.3.18 that allowed Mongrel to properly handle partial PUT requests. I sent the patch & tests to Zed for comment but never really heard back.I'll take a look this weekend, but you really should have tests to go with this just to make sure I'm trying it the way you are trying it.
Here are the patches against Mongrel 1.0 plus a test file.
# Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html # for more information. require 'test/unit' require 'net/http' require 'mongrel' require 'timeout' require 'fileutils' require File.dirname(__FILE__) + "/testhelp.rb" class TestHandler < Mongrel::HttpHandler attr_reader :ran_test def process(request, response) @ran_test = true response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n") end end class UploadHandler < Mongrel::HttpHandler attr_accessor :default_content_type attr_reader :path attr_accessor :file_size ONLY_HEAD_PUT="Only HEAD and PUT allowed.".freeze # You give it the path to the directory root def initialize(path) @path = File.expand_path(path) # this only gets sent in response to a HEAD request for which only # the Content-Length component really matters. Leave this as a nice # default @default_content_type = "text/plain; charset=ISO-8859-1".freeze end def accept_partial_requests? true end def partial_put?(params) # returns true if the request_method is PUT and the header # contains a Content-Range field params[Const::REQUEST_METHOD] == Const::PUT && params.has_key?('HTTP_CONTENT_RANGE') end # Checks if the given path can be served and returns the full path (or nil if not). def can_write(path_info) req_path = File.expand_path(File.join(@path,HttpRequest.unescape(path_info)), @path) req_dir = File.dirname(req_path) if req_dir.index(@path) == 0 and File.exist? req_dir # it's a directory and we can write to it return req_path if File.directory?(req_dir) && File.writable?(req_dir) else # does not exist or isn't in the right spot return nil end end # Sends the size of a file back to the requester def send_head(req_path, request, response) stat = File.stat(req_path) # first we setup the headers and status then we do a very fast send on the socket directly response.status = 200 header = response.header # set the mime type from our map based on the ending header[Const::CONTENT_TYPE] = @default_content_type # send a status with only content length response.send_status(stat.size) response.send_header self.file_size = stat.size # used so the test can peek at our internal state end def receive_file(req_path, request, response) # STDERR.puts("#receive_file req_path = #{req_path}") # STDERR.puts("#receive_file @path = [EMAIL PROTECTED]") case request.body when Tempfile # do a cheap mv using the TempFile handed off in the request object FileUtils.mv request.body.path, req_path when String # otherwise do a more expensive read-then-write to transfer # the data from the request.body into the file File.open(req_path, "wb+") { |f| f.write(request.body) } when StringIO File.open(req_path, "wb+") { |f| f.write(request.body.read) } end end def receive_partial_file(req_path, request) # glue the two pieces back together bytes_written = nil File.open(req_path, "ab") do |f| case request.body when StringIO, Tempfile bytes_written = f.write(request.body.read) when String bytes_written = f.write(request.body) end end # STDERR.puts("#receive_partial_file wrote #{bytes_written} bytes.") end def process(request, response) # Process the request to return a HEAD, receive a new PUT, or # receive a partial-body PUT req_method = request.params[Const::REQUEST_METHOD] || Const::HEAD req_path = can_write request.params[Const::PATH_INFO] begin if req_method == Const::HEAD send_head(req_path, request, response) elsif partial_put?(request.params) receive_partial_file(req_path, request) STDERR.puts "received partial file" elsif req_method == Const::PUT receive_file(req_path, request, response) response.start(200) {|head,out| out << "File created." } unless response.nil? STDERR.puts "received complete file" end rescue => details STDERR.puts "Error receiving file #{req_path}: #{details}" end end end class UploadWebServerTest < Test::Unit::TestCase def setup # make a directory in /tmp and put a large file into it for testing @src_path = '/tmp/mongrel_upload_test' src_file = 'source_file' FileUtils.mkdir_p(@src_path) @path_to_src_file = File.join(@src_path, src_file) File.open(@path_to_src_file, "w+") do |f| 20.times {|a| f.write(IO.read(File.expand_path(__FILE__))) } end @path_to_new_file = File.join(@src_path, 'upload.tst') @src_file_size = File.size(@path_to_src_file) @request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n" @server = HttpServer.new("127.0.0.1", 9998,num_processors=1) @uploader = UploadHandler.new(@src_path) @server.register("/uploads", @uploader) @tester = TestHandler.new @server.register("/test", @tester) redirect_test_io do @server.run end end def teardown @server.stop FileUtils.rm_r(@src_path) end def do_test(st, chunk, close_after=nil) s = TCPSocket.new("127.0.0.1", 9998); req = StringIO.new(st) nout = 0 while data = req.read(chunk) nout += s.write(data) s.flush sleep 0.2 if close_after and nout > close_after s.close_write sleep 1 end end s.write(" ") if RUBY_PLATFORM =~ /mingw|mswin|cygwin/ s.close end def test_complete_put request = "PUT /uploads/upload.tst HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Length: [EMAIL PROTECTED]: 100-continue\r\n\r\n" attached_file = IO.read(@path_to_src_file) redirect_test_io do do_test(request + attached_file, @src_file_size/2) end assert File.exists?(@path_to_new_file) end def test_file_remains_after_partial_put request = "PUT /uploads/upload.tst HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Length: [EMAIL PROTECTED]: 100-continue\r\n\r\n" attached_file = IO.read(@path_to_src_file) assert_raises IOError do redirect_test_io do do_test(request + attached_file, @src_file_size/2, (@src_file_size-1)/2) end end assert File.exists?(@path_to_new_file) end def test_get_head_on_partial_put setup_partial_file response = get_head assert_equal(File.size(@path_to_new_file), response['content-length'].to_i) end def test_resume_put setup_partial_file response = get_head offset = response['content-length'].to_i continue_at = "[EMAIL PROTECTED]/[EMAIL PROTECTED]" new_content_length = "[EMAIL PROTECTED] - offset}" new_request = "PUT /uploads/upload.tst HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Range: #{continue_at}\r\nContent-Length: #{new_content_length}\r\nExpected: 100-continue\r\n\r\n" file_remainder = IO.read(@path_to_src_file, offset) redirect_test_io do do_test(new_request + file_remainder, @src_file_size/2) end assert_equal(@src_file_size, File.size(@path_to_new_file)) end private def setup_partial_file File.open(@path_to_new_file, "w+") do |f| f.write(IO.read(@path_to_src_file, @src_file_size/2)) end end def get_head response = nil Net::HTTP.start('127.0.0.1', 9998) do |http| response = http.head('/uploads/upload.tst') end response end end
handlers.diff
Description: Binary data
mongrel.diff
Description: Binary data
Also, seeing how it can improve or change the upload progress problem (the fact that it sucks) would be a good first step.
That problem was a bit out of scope for me so I don't think this helps much. The common assumption is that people are using POST for uploads which this patch doesn't address at all.
Otherwise, I'll look and get back to you. Sorry I didn't get in touch with you earlier. Just been busy.
No problem; we're all busy! cr
_______________________________________________ Mongrel-users mailing list Mongrel-users@rubyforge.org http://rubyforge.org/mailman/listinfo/mongrel-users