1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
# frozen_string_literal: true
module HTTParty
class Request
class StreamingMultipartBody
NEWLINE = "\r\n"
CHUNK_SIZE = 64 * 1024 # 64 KB chunks
def initialize(parts, boundary)
@parts = parts
@boundary = boundary
@part_index = 0
@state = :header
@current_file = nil
@header_buffer = nil
@header_offset = 0
@footer_sent = false
end
def size
@size ||= calculate_size
end
def read(length = nil, outbuf = nil)
outbuf = outbuf ? outbuf.replace(''.b) : ''.b
return read_all(outbuf) if length.nil?
while outbuf.bytesize < length
chunk = read_chunk(length - outbuf.bytesize)
break if chunk.nil?
outbuf << chunk
end
outbuf.empty? ? nil : outbuf
end
def rewind
@part_index = 0
@state = :header
@current_file = nil
@header_buffer = nil
@header_offset = 0
@footer_sent = false
@parts.each do |_key, value, _is_file|
value.rewind if value.respond_to?(:rewind)
end
end
private
def read_all(outbuf)
while (chunk = read_chunk(CHUNK_SIZE))
outbuf << chunk
end
outbuf.empty? ? nil : outbuf
end
def read_chunk(max_length)
loop do
return nil if @part_index >= @parts.size && @footer_sent
if @part_index >= @parts.size
@footer_sent = true
return "--#{@boundary}--#{NEWLINE}".b
end
key, value, is_file = @parts[@part_index]
case @state
when :header
chunk = read_header_chunk(key, value, is_file, max_length)
return chunk if chunk
when :body
chunk = read_body_chunk(value, is_file, max_length)
return chunk if chunk
when :newline
@state = :header
@part_index += 1
return NEWLINE.b
end
end
end
def read_header_chunk(key, value, is_file, max_length)
if @header_buffer.nil?
@header_buffer = build_part_header(key, value, is_file)
@header_offset = 0
end
remaining = @header_buffer.bytesize - @header_offset
if remaining > 0
chunk_size = [remaining, max_length].min
chunk = @header_buffer.byteslice(@header_offset, chunk_size)
@header_offset += chunk_size
return chunk
end
@header_buffer = nil
@header_offset = 0
@state = :body
nil
end
def read_body_chunk(value, is_file, max_length)
if is_file
chunk = read_file_chunk(value, max_length)
if chunk
return chunk
else
@current_file = nil
@state = :newline
return nil
end
else
@state = :newline
return value.to_s.b
end
end
def read_file_chunk(file, max_length)
chunk_size = [max_length, CHUNK_SIZE].min
chunk = file.read(chunk_size)
return nil if chunk.nil?
chunk.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
chunk
end
def build_part_header(key, value, is_file)
header = "--#{@boundary}#{NEWLINE}".b
header << %(Content-Disposition: form-data; name="#{key}").b
if is_file
header << %(; filename="#{file_name(value).gsub(/["\r\n]/, replacement_table)}").b
header << NEWLINE.b
header << "Content-Type: #{content_type(value)}#{NEWLINE}".b
else
header << NEWLINE.b
end
header << NEWLINE.b
header
end
def calculate_size
total = 0
@parts.each do |key, value, is_file|
total += build_part_header(key, value, is_file).bytesize
total += content_size(value, is_file)
total += NEWLINE.bytesize
end
total += "--#{@boundary}--#{NEWLINE}".bytesize
total
end
def content_size(value, is_file)
if is_file
if value.respond_to?(:size)
value.size
elsif value.respond_to?(:stat)
value.stat.size
else
value.read.bytesize.tap { value.rewind }
end
else
value.to_s.b.bytesize
end
end
def content_type(object)
return object.content_type if object.respond_to?(:content_type)
require 'mini_mime'
mime = MiniMime.lookup_by_filename(object.path)
mime ? mime.content_type : 'application/octet-stream'
end
def file_name(object)
object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
end
def replacement_table
@replacement_table ||= {
'"' => '%22',
"\r" => '%0D',
"\n" => '%0A'
}.freeze
end
end
end
end
|