Skip to content

Instantly share code, notes, and snippets.

@jcasts
Last active December 11, 2015 04:59
Show Gist options
  • Save jcasts/4549450 to your computer and use it in GitHub Desktop.
Save jcasts/4549450 to your computer and use it in GitHub Desktop.
class Base64IO
def initialize io
@io = io
@buff = ""
@eof = false
end
def eof?
@eof
end
def size
return unless @io.size
((@io.size / 3.0).ceil * 4) + 1
end
def close
@io.close
end
def closed?
@io.closed?
end
def rewind
if @io.rewind == 0
@buff = ""
@eof = false
0
end
end
def read_all outbuf=nil, nonblock=false
output = ""
while str = read(1024, outbuf, nonblock); output << str; end
output.empty? ? nil : output
end
def read_nonblock bytes, outbuf=nil
raise TypeError, "no implicit conversion from nil to integer" unless
Integer === bytes
read bytes, outbuf, true
end
def read bytes=nil, outbuf=nil, nonblock=false
raise EOFError, "end of file reached" if @eof && nonblock
return if @eof
return read_all(outbuf, nonblock) if bytes.nil?
out = @buff.slice!(0,bytes)
if out.bytesize < bytes
meth = nonblock ? :read_nonblock : :read
iostr = @io.send(meth, iobytes(bytes - out.bytesize)).to_s
out << [iostr].pack('m')[0...-1]
@buff = out.slice!(bytes..-1).to_s
end
if out.length < bytes
out << "\n"
@eof = true
end
outbuf << out if outbuf
out
end
private
##
# Find the number pre-base64 bytes to read, then get
# the closest multiple of 3 >= to it (to prevent padding)
def iobytes bytes
(bytes / 4.0).ceil * 3
end
end
##
# Builder for the body of a multipart request.
class Multipart
# An array of parts for the multipart body.
attr_reader :parts
# The separator used between parts.
attr_reader :boundary
def initialize boundary
@boundary = boundary
@parts = []
end
##
# Add a new part to the body which will be base64 encoded before writing.
# Supports String and IO values.
def add_encode64 name, value, headers=nil
headers ||= {}
headers['Content-Transfer-Encoding'] = 'BASE64'
if value.respond_to?(:read)
value = Base64IO.new value
else
value = [value].pack('m')
end
add name, value, headers
end
##
# Add a new part to the body.
# Supports String and IO values.
def add name, value, headers=nil
headers ||= {}
headers['content-disposition'] = "form-data; name=\"#{name}\""
if value.respond_to?(:path)
headers['content-disposition'] <<
"; filename=\"#{File.basename value.path}\""
headers['Content-Type'] ||= MIME::Types.of(value.path)[0]
headers['Content-Type'] &&= headers['Content-Type'].to_s
end
if value.respond_to?(:read)
headers['Content-Type'] ||= "application/octet-stream"
headers['Content-Transfer-Encoding'] ||= 'binary'
end
parts << [headers, value]
end
##
# Convert the instance into a MultipartIO instance.
def to_io
io = MultipartIO.new
buff = ""
parts.each do |(headers, value)|
buff << "--#{@boundary}\r\n"
buff << "content-disposition: #{headers['content-disposition']}\r\n"
headers.each do |hname, hvalue|
next if hname == 'content-disposition'
hvalue = hvalue.to_s.inspect if hvalue.to_s.index ":"
buff << "#{hname}: #{hvalue}\r\n"
end
buff << "\r\n"
if value.respond_to?(:read)
io.add buff.dup
io.add value
buff.replace ""
else
buff << value.to_s
end
buff << "\r\n"
end
buff << "--#{@boundary}--"
io.add buff
io
end
end
class MultipartIO
attr_reader :parts, :curr_part
def initialize *parts
@parts = []
@curr_part = 0
parts.each do |part|
add part
end
end
def add part
if String === part
@parts << StringIO.new(part)
elsif part.respond_to?(:read)
@parts << part
else
raise ArgumentError, "Invalid part #{part.inspect}"
end
@curr_part ||= @parts.length - 1
@parts.last
end
def close
@parts.each(&:close)
nil
end
def read bytes=nil, outbuf=nil
return read_all(outbuf) if bytes.nil?
return if @parts.empty? || eof?
buff = ""
until @curr_part.nil?
bytes = bytes - buff.bytes.count
buff << @parts[@curr_part].read(bytes).to_s
break if buff.bytes.count >= bytes
@curr_part += 1
@curr_part = nil if @curr_part >= @parts.length
end
return if buff.empty?
outbuf << buff if outbuf
buff
end
def read_all outbuf=nil
return if eof?
output = @parts[@curr_part..-1].inject("") do |out, curr|
@curr_part += 1
out << curr.read
end
@curr_part = nil
outbuf << output if outbuf
output
end
def eof?
@curr_part.nil?
end
def size
total = 0
@parts.each do |part|
return nil unless part.respond_to?(:size) && part.size
total += part.size
end
total
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment