about summary refs log tree commit homepage
path: root/lib/yahns/tee_input.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/yahns/tee_input.rb')
-rw-r--r--lib/yahns/tee_input.rb114
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