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.

Body is assembled as a single binary-encoded String – file contents are read into memory rather than streamed. This matches the PHP SDK’s approach; callers scanning very large files should be aware.

Class Method Summary collapse

Class Method Details

.encode(fields, file_path, file_field: "file") ⇒ Array(String, String)

Encode a multipart body containing the bytes at file_path plus the given text fields.

Parameters:

  • fields (Hash{String=>String})

    text form fields (e.g. metadata => v, callback => url)

  • file_path (String)

    path to the file to upload

  • file_field (String) (defaults to: "file")

    name of the file form field; default “file”

Returns:

  • (Array(String, String))

    tuple of [body, content_type]



32
33
34
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 32

def encode(fields, file_path, file_field: "file")
  boundary = make_boundary

  filename = File.basename(file_path)
  content_type = guess_content_type(file_path)
  file_bytes = File.binread(file_path)

  body = String.new(encoding: Encoding::BINARY)

  fields.each do |name, value|
    write_text_part(body, boundary, name.to_s, value.to_s)
  end

  body << "--#{boundary}\r\n".b
  body << "Content-Disposition: form-data; name=\"#{file_field}\"; filename=\"#{filename}\"\r\n".b
  body << "Content-Type: #{content_type}\r\n\r\n".b
  body << file_bytes.b
  body << "\r\n".b
  body << "--#{boundary}--\r\n".b

  [body, make_content_type(boundary)]
end

.guess_content_type(path) ⇒ Object

Best-effort content-type lookup by 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(path)
  ext = File.extname(path).delete_prefix(".").downcase
  MIME_TYPES.fetch(ext, "application/octet-stream")
end

.make_boundaryObject

Generate a unique multipart boundary.



16
17
18
# File 'lib/scanii/multipart.rb', line 16

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.



21
22
23
# File 'lib/scanii/multipart.rb', line 21

def make_content_type(boundary)
  "multipart/form-data; boundary=#{boundary}"
end