Module: Scanii::Multipart
- Defined in:
- lib/scanii/multipart.rb
Overview
Hand-rolled multipart/form-data encoder (RFC 7578).
Ruby’s stdlib Net::HTTP does not bundle a multipart encoder; this is the smallest viable implementation that covers the Scanii POST /files payload.
Class Method Summary collapse
-
.guess_content_type(filename) ⇒ Object
Best-effort content-type lookup by filename extension.
-
.make_boundary ⇒ Object
Generate a unique multipart boundary.
-
.make_content_type(boundary) ⇒ Object
Build the Content-Type header value for a request using boundary.
-
.stream_encode(fields, io, filename, content_type = nil, file_field: "file") ⇒ Array(ChainedIO, String, Integer)
Encode a multipart body as a streaming ChainedIO.
Class Method Details
.guess_content_type(filename) ⇒ Object
Best-effort content-type lookup by filename extension. Falls back to application/octet-stream. The Scanii API does not require an accurate content-type on the multipart part – the server inspects the bytes – so a short table is sufficient.
59 60 61 62 |
# File 'lib/scanii/multipart.rb', line 59 def guess_content_type(filename) ext = File.extname(filename.to_s).delete_prefix(".").downcase MIME_TYPES.fetch(ext, "application/octet-stream") end |
.make_boundary ⇒ Object
Generate a unique multipart boundary.
13 14 15 |
# File 'lib/scanii/multipart.rb', line 13 def make_boundary "----scanii-ruby-boundary-#{SecureRandom.hex(16)}" end |
.make_content_type(boundary) ⇒ Object
Build the Content-Type header value for a request using boundary.
18 19 20 |
# File 'lib/scanii/multipart.rb', line 18 def make_content_type(boundary) "multipart/form-data; boundary=#{boundary}" end |
.stream_encode(fields, io, filename, content_type = nil, file_field: "file") ⇒ Array(ChainedIO, String, Integer)
Encode a multipart body as a streaming ChainedIO.
Builds the RFC 7578 prologue and epilogue as binary Strings, chains them around the caller’s IO, and returns the triple required for Net::HTTP body_stream= uploads. The caller’s IO is never read here – only when Net::HTTP reads from the returned ChainedIO.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/scanii/multipart.rb', line 35 def stream_encode(fields, io, filename, content_type = nil, file_field: "file") boundary = make_boundary ct = content_type || guess_content_type(filename) prologue = String.new(encoding: Encoding::BINARY) fields.each do |name, value| write_text_part(prologue, boundary, name.to_s, value.to_s) end prologue << "--#{boundary}\r\n".b prologue << "Content-Disposition: form-data; name=\"#{file_field}\"; filename=\"#{filename}\"\r\n".b prologue << "Content-Type: #{ct}\r\n\r\n".b epilogue = "\r\n--#{boundary}--\r\n".b io_size = io_remaining_bytes(io) total_length = prologue.bytesize + io_size + epilogue.bytesize [ChainedIO.new(prologue, io, epilogue), make_content_type(boundary), total_length] end |