Class: Api2Convert::Upload::MultipartStream
- Inherits:
-
Object
- Object
- Api2Convert::Upload::MultipartStream
- Defined in:
- lib/api2convert/upload/multipart_stream.rb
Overview
A read-only IO that concatenates a multipart preamble, the file body IO, and
the trailing boundary — so the body is streamed to the socket rather than
loaded into memory. Net::HTTP#body_stream= reads it via #read(len).
Class Method Summary collapse
-
.build(boundary, field, filename, io) ⇒ Object
Build the streamed body for a single
filefield. -
.escape(value) ⇒ Object
Strip quotes and CR/LF from a header parameter so it can't break out of the multipart header (header-injection defense).
Instance Method Summary collapse
-
#initialize(preamble, io, epilogue) ⇒ MultipartStream
constructor
A new instance of MultipartStream.
-
#read(length = nil, outbuf = nil) ⇒ Object
Read up to
lengthbytes across the parts.
Constructor Details
#initialize(preamble, io, epilogue) ⇒ MultipartStream
Returns a new instance of MultipartStream.
39 40 41 42 |
# File 'lib/api2convert/upload/multipart_stream.rb', line 39 def initialize(preamble, io, epilogue) @parts = [StringIO.new(preamble), io, StringIO.new(epilogue)] @index = 0 end |
Class Method Details
.build(boundary, field, filename, io) ⇒ Object
Build the streamed body for a single file field. Returns
[stream, content_length]; the caller sets Content-Length from the
length so Net::HTTP does not fall back to chunked encoding.
io must be positioned at the start and expose a byte #size (a File or
a StringIO both do — the uploader coerces anything else into a StringIO).
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/api2convert/upload/multipart_stream.rb', line 17 def self.build(boundary, field, filename, io) disposition = %(form-data; name="#{escape(field)}"; filename="#{escape(filename)}") preamble = [ "--#{boundary}", "Content-Disposition: #{disposition}", "Content-Type: application/octet-stream", "", "" ].join("\r\n").b epilogue = "\r\n--#{boundary}--\r\n".b body_size = io.size - (io.respond_to?(:pos) ? io.pos : 0) length = preamble.bytesize + body_size + epilogue.bytesize [new(preamble, io, epilogue), length] end |
.escape(value) ⇒ Object
Strip quotes and CR/LF from a header parameter so it can't break out of the multipart header (header-injection defense).
35 36 37 |
# File 'lib/api2convert/upload/multipart_stream.rb', line 35 def self.escape(value) value.to_s.gsub('"', "%22").gsub(/[\r\n]/, "") end |
Instance Method Details
#read(length = nil, outbuf = nil) ⇒ Object
Read up to length bytes across the parts. With no length, reads all that
remains. Returns nil at end of stream (the contract Net::HTTP expects).
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/api2convert/upload/multipart_stream.rb', line 46 def read(length = nil, outbuf = nil) buffer = outbuf || "".b buffer.clear if length.nil? while @index < @parts.length chunk = @parts[@index].read buffer << chunk unless chunk.nil? @index += 1 end return buffer end while buffer.bytesize < length && @index < @parts.length chunk = @parts[@index].read(length - buffer.bytesize) if chunk.nil? @index += 1 else buffer << chunk end end return nil if buffer.empty? && @index >= @parts.length buffer end |