Class: Api2Convert::Upload::MultipartStream

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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