Index: lib/mongrel/handlers.rb =================================================================== --- lib/mongrel/handlers.rb (revision 168) +++ lib/mongrel/handlers.rb (working copy) @@ -10,9 +10,17 @@ # should be implemented using the HttpHandlerPlugin mixin. # class HttpHandler - def process(request, response) end + + protected + def respond_to(*types, &block) + raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block + block ||= lambda { |responder| types.each { |type| responder.send(type) } } + responder = Mime::Responder.new(block.binding) + block.call(responder) + responder.respond + end end Index: lib/mongrel/progress.rb =================================================================== --- lib/mongrel/progress.rb (revision 0) +++ lib/mongrel/progress.rb (revision 0) @@ -0,0 +1,96 @@ +module Mongrel + module Uploads + @guard = Mutex.new + @counters = {} + class << self + def check(upid) + @counters[upid] + end + def add(upid, size) + stats = {'size' => size, 'received' => 0} + @guard.synchronize do + @counters[upid] = stats + end + end + def mark(upid, len) + upload = @counters[upid] + recvd = upload['size'] - len + @guard.synchronize do + upload['received'] = recvd + end + end + def finish(upid) + upload = @counters[upid] + recvd = upload['size'] + @guard.synchronize do + upload['received'] = recvd + end + end + end + end + class ProgressHandler < Mongrel::HttpHandler + def process(request, response) + qs = HttpRequest.query_parse(request.params['QUERY_STRING']) + status = Uploads.check(qs['upload_id']) + response.start(200) do |head,out| + head['Content-Type'] = 'text/plain' + respond_to do |accepts| + accepts.html { out.write("size: #{status['size']}\nreceived: #{status['received']}\n") } + accepts.js do + head['Content-Type'] = 'text/javascript' + out.write("{size:#{status['size']},received:#{status['received']}}") + end + end if status + end + end + end + class HttpRequest + def initialize(params, initial_body, socket) + @params = params + @socket = socket + + clen = params[Const::CONTENT_LENGTH].to_i - initial_body.length + upload_id = nil + + if params['REQUEST_METHOD'] == 'POST' + qs = self.class.query_parse(params['QUERY_STRING']) + if qs['upload_id'] and not qs['upload_id'].empty? + upload_id = qs['upload_id'] + Uploads.add(upload_id, clen) if upload_id + end + end + + if clen > Const::MAX_BODY + @body = Tempfile.new(self.class.name) + @body.binmode + else + @body = StringIO.new + end + + begin + @body.write(initial_body) + + # write the odd sized chunk first + clen -= @body.write(@socket.read(clen % Const::CHUNK_SIZE)) + + # then stream out nothing but perfectly sized chunks + while clen > 0 + data = @socket.read(Const::CHUNK_SIZE) + # have to do it this way since @socket.eof? causes it to block + raise "Socket closed or read failure" if not data or data.length != Const::CHUNK_SIZE + clen -= @body.write(data) + Uploads.mark(upload_id, clen) if upload_id + end + + # rewind to keep the world happy + Uploads.finish(upload_id) if upload_id + @body.rewind + rescue Object + # any errors means we should delete the file, including if the file is dumped + STDERR.puts "Error reading request: #$!" + @body.delete if @body.class == Tempfile + @body = nil # signals that there was a problem + end + end + end +end Index: lib/mongrel.rb =================================================================== --- lib/mongrel.rb (revision 168) +++ lib/mongrel.rb (working copy) @@ -202,6 +202,19 @@ end end + def content_type + @content_type ||= Mime::Type.lookup(@params['CONTENT_TYPE'].to_s.downcase) + end + + def accepts + @accepts ||= + if @params['HTTP_ACCEPT'].to_s.strip.empty? + [ content_type, Mime::ALL ] + else + Mime::Type.parse(@params['HTTP_ACCEPT']) + end + end + # Performs URI escaping so that you can construct proper # query strings faster. Use this rather than the cgi.rb # version since it's faster. (Stolen from Camping). Index: lib/mime/responder.rb =================================================================== --- lib/mime/responder.rb (revision 0) +++ lib/mime/responder.rb (revision 0) @@ -0,0 +1,52 @@ +module Mime + class Responder #:nodoc: + def initialize(block_binding) + @block_binding = block_binding + @mime_type_priority = eval("request.accepts", block_binding) + @order = [] + @responses = {} + end + + def custom(mime_type, &block) + mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + @order << mime_type + @responses[mime_type] = block + end + + for mime_type in %w( all html js xml rss atom yaml ) + eval <<-EOT + def #{mime_type}(&block) + custom(Mime::#{mime_type.upcase}, &block) + end + EOT + end + + def unacceptable(&block) + custom(:unacceptable, &block) + end + + def any(*args, &block) + args.each { |type| send(type, &block) } + end + + def respond + for priority in @mime_type_priority + if priority == Mime::ALL + @responses[@order.first].call + return + else + if priority === @order + @responses[priority].call + return # mime type match found, be happy and return + end + end + end + + if @order.include?(Mime::ALL) + @responses[Mime::ALL].call + else + @responses[:unacceptable].call if @responses[:unacceptable] + end + end + end +end \ No newline at end of file Index: lib/mime/mime_type.rb =================================================================== --- lib/mime/mime_type.rb (revision 0) +++ lib/mime/mime_type.rb (revision 0) @@ -0,0 +1,142 @@ +module Mime + class Type #:nodoc: + # A simple helper class used in parsing the accept header + class AcceptItem #:nodoc: + attr_accessor :order, :name, :q + + def initialize(order, name, q=nil) + @order = order + @name = name.strip + q ||= 0.0 if @name == "*/*" # default "*/*" to end of list + @q = ((q || 1.0).to_f * 100).to_i + end + + def to_s + @name + end + + def <=>(item) + result = item.q <=> q + result = order <=> item.order if result == 0 + result + end + + def ==(item) + name == (item.respond_to?(:name) ? item.name : item) + end + end + + class << self + def lookup(string) + LOOKUP[string] + end + + def parse(accept_header) + # keep track of creation order to keep the subsequent sort stable + index = 0 + list = accept_header.split(/,/). + map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort! + + # Take care of the broken text/xml entry by renaming or deleting it + + text_xml = list.index("text/xml") + app_xml = list.index("application/xml") + + if text_xml && app_xml + # set the q value to the max of the two + list[app_xml].q = [list[text_xml].q, list[app_xml].q].max + + # make sure app_xml is ahead of text_xml in the list + if app_xml > text_xml + list[app_xml], list[text_xml] = list[text_xml], list[app_xml] + app_xml, text_xml = text_xml, app_xml + end + + # delete text_xml from the list + list.delete_at(text_xml) + + elsif text_xml + list[text_xml].name = "application/xml" + end + + # Look for more specific xml-based types and sort them ahead of app/xml + + if app_xml + idx = app_xml + app_xml_type = list[app_xml] + + while(idx < list.length) + type = list[idx] + break if type.q < app_xml_type.q + if type.name =~ /\+xml$/ + list[app_xml], list[idx] = list[idx], list[app_xml] + app_xml = idx + end + idx += 1 + end + end + + list.map! { |i| Mime::Type.lookup(i.name) }.uniq! + list + end + end + + def initialize(string, symbol = nil, synonyms = []) + @symbol, @synonyms = symbol, synonyms + @string = string + end + + def to_s + @string + end + + def to_str + to_s + end + + def to_sym + @symbol || @string.to_sym + end + + def ===(list) + if list.is_a?(Array) + (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) } + else + super + end + end + + def ==(mime_type) + (@synonyms + [ self ]).any? { |synonym| synonym.to_s == mime_type.to_s } if mime_type + end + end + + ALL = Type.new "*/*", :all + HTML = Type.new "text/html", :html, %w( application/xhtml+xml ) + JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript ) + XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml ) + RSS = Type.new "application/rss+xml", :rss + ATOM = Type.new "application/atom+xml", :atom + YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml ) + + LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) } + + LOOKUP["*/*"] = ALL + + LOOKUP["text/html"] = HTML + LOOKUP["application/xhtml+xml"] = HTML + + LOOKUP["application/xml"] = XML + LOOKUP["text/xml"] = XML + LOOKUP["application/x-xml"] = XML + + LOOKUP["text/javascript"] = JS + LOOKUP["application/javascript"] = JS + LOOKUP["application/x-javascript"] = JS + + LOOKUP["text/yaml"] = YAML + LOOKUP["application/x-yaml"] = YAML + + LOOKUP["application/rss+xml"] = RSS + LOOKUP["application/atom+xml"] = ATOM +end \ No newline at end of file