Supabase.Fetcher.Multipart (supabase_potion v0.8.0)

Provides multipart/form-data encoding for file uploads and form submissions.

Wraps the multipart library with a Supabase-friendly API.

Part Types

  • {:file, binary, opts} - File from memory
  • {:file_path, path, opts} - File from disk (streaming)
  • {:field, name, value} - Text field
  • {:field, name, value, opts} - Text field with options

Examples

parts = [
  {:file_path, "/path/to/doc.pdf", name: "document"},
  {:field, "description", "Monthly report"}
]

{content_type, body} = Supabase.Fetcher.Multipart.encode(parts)

File Upload from Memory

file_binary = File.read!("report.pdf")
parts = [
  {:file, file_binary,
   name: "document",
   filename: "report.pdf",
   content_type: "application/pdf"}
]

Supabase.Functions.invoke(client, "upload", body: {:multipart, parts})

File Upload from Disk (Streaming)

parts = [
  {:file_path, "/path/to/large-video.mp4", name: "video"}
]

Supabase.Functions.invoke(client, "process-video", body: {:multipart, parts})

Multiple Files with Metadata

parts = [
  {:file_path, "/path/to/image1.jpg", name: "photos", filename: "photo1.jpg"},
  {:file_path, "/path/to/image2.jpg", name: "photos", filename: "photo2.jpg"},
  {:field, "album", "Vacation 2025"},
  {:field, "tags", "beach,sunset"}
]

Supabase.Functions.invoke(client, "upload-album", body: {:multipart, parts})

Phoenix/Plug Integration

def upload_to_function(conn, _params) do
  %Plug.Upload{path: path, filename: filename, content_type: type} =
    conn.params["file"]

  parts = [
    {:file_path, path,
     name: "file",
     filename: filename,
     content_type: type},
    {:field, "user_id", conn.assigns.user_id}
  ]

  case Supabase.Functions.invoke(client, "process-upload",
         body: {:multipart, parts}) do
    {:ok, response} -> json(conn, response.body)
    {:error, error} -> put_status(conn, 500) |> json(%{error: error})
  end
end

Large File Streaming

parts = [
  {:file_path, "/path/to/huge-file.zip", name: "backup"}
]

# Use multipart_stream for very large files
Supabase.Functions.invoke(client, "backup",
  body: {:multipart_stream, parts},
  timeout: 60_000  # 1 minute timeout
)

Summary

Functions

Calculates the total content length without loading files into memory.

Encodes parts as multipart/form-data.

Encodes parts as streaming multipart/form-data.

Types

part()

@type part() ::
  {:file, binary(), opts :: keyword()}
  | {:file_path, Path.t(), opts :: keyword()}
  | {:field, name :: String.t(), value :: String.t()}
  | {:field, name :: String.t(), value :: String.t(), opts :: keyword()}

parts()

@type parts() :: [part()]

Functions

content_length(parts)

@spec content_length(parts()) :: non_neg_integer()

Calculates the total content length without loading files into memory.

This is useful for setting the Content-Length header or for progress tracking.

Examples

parts = [
  {:file_path, "/path/to/file.pdf", name: "document"}
]

length = Multipart.content_length(parts)
# => 524288  (bytes)

encode(parts)

@spec encode(parts()) :: {content_type :: String.t(), body :: iodata()}

Encodes parts as multipart/form-data.

Returns {content_type, body} where content_type includes the boundary. Body is returned as iodata for efficient memory usage.

Options for file parts

  • :name (required) - Form field name
  • :filename - Custom filename (defaults to basename for file_path)
  • :content_type - Custom content-type (defaults to auto-detection)

Options for field parts

  • :content_type - Custom content-type (defaults to none)

Examples

parts = [
  {:file, "PDF content", name: "doc", filename: "report.pdf"},
  {:field, "description", "Q4 Report"}
]

{content_type, body} = Multipart.encode(parts)
# => {"multipart/form-data; boundary=...", <<...>>}

encode_stream(parts)

@spec encode_stream(parts()) :: {content_type :: String.t(), stream :: Enumerable.t()}

Encodes parts as streaming multipart/form-data.

Returns {content_type, stream} where stream is an Enumerable. Useful for large file uploads to reduce memory usage.

Examples

parts = [{:file_path, "/large/file.zip", name: "backup"}]

{content_type, stream} = Multipart.encode_stream(parts)

# Stream can be consumed chunk by chunk
Enum.each(stream, fn chunk ->
  # Process chunk
end)