diff options
Diffstat (limited to 'lib/yahns/tee_input.rb')
-rw-r--r-- | lib/yahns/tee_input.rb | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/lib/yahns/tee_input.rb b/lib/yahns/tee_input.rb new file mode 100644 index 0000000..0d91a89 --- /dev/null +++ b/lib/yahns/tee_input.rb @@ -0,0 +1,114 @@ +# -*- encoding: binary -*- +# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al. +# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt) + +# acts like tee(1) on an input input to provide a input-like stream +# while providing rewindable semantics through a File/StringIO backing +# store. On the first pass, the input is only read on demand so your +# Rack application can use input notification (upload progress and +# like). This should fully conform to the Rack::Lint::InputWrapper +# specification on the public API. This class is intended to be a +# strict interpretation of Rack::Lint::InputWrapper functionality and +# will not support any deviations from it. +# +# When processing uploads, Yahns exposes a TeeInput object under +# "rack.input" of the Rack environment. +class Yahns::TeeInput < Yahns::StreamInput # :nodoc: + # Initializes a new TeeInput object. You normally do not have to call + # this unless you are writing an HTTP server. + def initialize(client, request) + @len = request.content_length + super + @tmp = client.class.tmpio_for(@len) + end + + # :call-seq: + # ios.size => Integer + # + # Returns the size of the input. For requests with a Content-Length + # header value, this will not read data off the socket and just return + # the value of the Content-Length header as an Integer. + # + # For Transfer-Encoding:chunked requests, this requires consuming + # all of the input stream before returning since there's no other + # way to determine the size of the request body beforehand. + # + # This method is no longer part of the Rack specification as of + # Rack 1.2, so its use is not recommended. This method only exists + # for compatibility with Rack applications designed for Rack 1.1 and + # earlier. Most applications should only need to call +read+ with a + # specified +length+ in a loop until it returns +nil+. + def size + @len and return @len + pos = @tmp.pos + consume! + @tmp.pos = pos + @len = @tmp.size + end + + # :call-seq: + # ios.read([length [, buffer ]]) => string, buffer, or nil + # + # Reads at most length bytes from the I/O stream, or to the end of + # file if length is omitted or is nil. length must be a non-negative + # integer or nil. If the optional buffer argument is present, it + # must reference a String, which will receive the data. + # + # At end of file, it returns nil or "" depend on length. + # ios.read() and ios.read(nil) returns "". + # ios.read(length [, buffer]) returns nil. + # + # If the Content-Length of the HTTP request is known (as is the common + # case for POST requests), then ios.read(length [, buffer]) will block + # until the specified length is read (or it is the last chunk). + # Otherwise, for uncommon "Transfer-Encoding: chunked" requests, + # ios.read(length [, buffer]) will return immediately if there is + # any data and only block when nothing is available (providing + # IO#readpartial semantics). + def read(*args) + @client ? tee(super) : @tmp.read(*args) + end + + # :call-seq: + # ios.gets => string or nil + # + # Reads the next ``line'' from the I/O stream; lines are separated + # by the global record separator ($/, typically "\n"). A global + # record separator of nil reads the entire unread contents of ios. + # Returns nil if called at the end of file. + # This takes zero arguments for strict Rack::Lint compatibility, + # unlike IO#gets. + def gets + @client ? tee(super) : @tmp.gets + end + + # :call-seq: + # ios.rewind => 0 + # + # Positions the *ios* pointer to the beginning of input, returns + # the offset (zero) of the +ios+ pointer. Subsequent reads will + # start from the beginning of the previously-buffered input. + def rewind + return 0 if 0 == @tmp.size + consume! if @client + @tmp.rewind # Rack does not specify what the return value is here + end + + # consumes the stream of the socket + def consume! + junk = "" + rsize = __rsize + nil while read(rsize, junk) + end + + def tee(buffer) + if buffer && buffer.size > 0 + @tmp.write(buffer) + end + buffer + end + + def discard + @tmp = @tmp.close + end +end |