Current section
Files
Jump to
Current section
Files
lib/tinkex/multipart/encoder.ex
defmodule Tinkex.Multipart.Encoder do
@moduledoc """
Build multipart/form-data request bodies (Finch does not encode multipart for us).
"""
@crlf "\r\n"
@spec encode_multipart(map(), map() | list(), String.t() | nil) ::
{:ok, binary(), String.t()} | {:error, term()}
def encode_multipart(form_fields, files, boundary \\ nil)
def encode_multipart(form_fields, files, boundary) when is_map(form_fields) do
resolved_boundary = boundary || generate_boundary()
with {:ok, parts} <- encode_parts(resolved_boundary, form_fields, files) do
body = IO.iodata_to_binary([parts, "--", resolved_boundary, "--", @crlf])
{:ok, body, "multipart/form-data; boundary=#{resolved_boundary}"}
end
end
def encode_multipart(_form_fields, _files, _boundary), do: {:error, :invalid_form_fields}
@spec generate_boundary() :: String.t()
def generate_boundary do
16
|> :crypto.strong_rand_bytes()
|> Base.encode16(case: :lower)
end
defp encode_parts(boundary, form_fields, files) do
with {:ok, field_parts} <- encode_form_fields(boundary, form_fields),
{:ok, file_parts} <- encode_files(boundary, files) do
{:ok, [field_parts, file_parts]}
end
end
defp encode_form_fields(boundary, form_fields) do
parts =
form_fields
|> Enum.flat_map(fn {name, value} ->
case value do
list when is_list(list) ->
Enum.map(list, &encode_field_part(boundary, name, &1))
other ->
[encode_field_part(boundary, name, other)]
end
end)
{:ok, parts}
end
defp encode_files(_boundary, nil), do: {:ok, []}
defp encode_files(boundary, files) when is_map(files) do
parts =
Enum.map(files, fn {name, file} ->
encode_file_part(boundary, name, normalize_file(file))
end)
{:ok, parts}
end
defp encode_files(boundary, files) when is_list(files) do
parts =
Enum.map(files, fn {name, file} ->
encode_file_part(boundary, name, normalize_file(file))
end)
{:ok, parts}
end
defp encode_files(_boundary, files), do: {:error, {:invalid_files, files}}
defp encode_field_part(boundary, name, value) do
[
"--",
boundary,
@crlf,
"Content-Disposition: form-data; name=\"",
to_string(name),
"\"",
@crlf,
@crlf,
to_string(value),
@crlf
]
end
defp encode_file_part(boundary, name, {filename, content, content_type, headers}) do
disposition =
case filename do
nil ->
["Content-Disposition: form-data; name=\"", to_string(name), "\"", @crlf]
_ ->
[
"Content-Disposition: form-data; name=\"",
to_string(name),
"\"; filename=\"",
filename,
"\"",
@crlf
]
end
type_line = ["Content-Type: ", content_type || "application/octet-stream", @crlf]
header_lines =
headers
|> Enum.map(fn {k, v} -> [to_string(k), ": ", to_string(v), @crlf] end)
[
"--",
boundary,
@crlf,
disposition,
type_line,
header_lines,
@crlf,
content,
@crlf
]
end
defp normalize_file({filename, content}) do
{filename, content, nil, []}
end
defp normalize_file({filename, content, content_type}) do
{filename, content, content_type, []}
end
defp normalize_file({filename, content, content_type, headers}) do
{filename, content, content_type, normalize_headers(headers)}
end
defp normalize_file(content) do
{nil, content, nil, []}
end
defp normalize_headers(headers) when is_map(headers), do: Map.to_list(headers)
defp normalize_headers(headers) when is_list(headers), do: headers
defp normalize_headers(_), do: []
end