Powered by AppSignal & Oban Pro

MTOM Attachments and Binary Data Handling

livebooks/mtom_attachments.livemd

MTOM Attachments and Binary Data Handling

Mix.install([
  {:lather, "~> 1.0"},
  {:finch, "~> 0.18"},
  {:kino, "~> 0.12"}
])

Introduction to MTOM

MTOM (Message Transmission Optimization Mechanism) is a W3C recommendation for efficiently transmitting binary data within SOAP messages. Instead of base64 encoding binary content directly in the XML, MTOM uses XOP (XML-binary Optimized Packaging) to package binary data as separate MIME parts.

Why MTOM?

When you need to send binary files (PDFs, images, documents) via SOAP, you have two main options:

  1. Base64 Encoding: Embed the binary data directly in XML

    • Simple to implement
    • Increases message size by ~33%
    • All data must be parsed as XML
  2. MTOM/XOP: Package binary data as separate MIME parts

    • More complex message structure
    • No size overhead for binary data
    • Binary data bypasses XML parsing

When to Use MTOM

Scenario Recommendation
Small files (< 1KB) Base64 is simpler
Medium files (1KB - 100KB) Either works, MTOM slightly better
Large files (> 100KB) MTOM strongly recommended
High-volume attachments MTOM for performance
Simple integration Base64 for simplicity

MTOM Message Structure

An MTOM message is a multipart/related MIME package:

Content-Type: multipart/related; boundary="uuid:xxx"; type="application/xop+xml"

--uuid:xxx
Content-Type: application/xop+xml; charset=UTF-8
Content-ID: 


  
    
      
        
      
    
  


--uuid:xxx
Content-Type: application/pdf
Content-ID: 
Content-Transfer-Encoding: binary

%PDF-1.4 [binary data]...
--uuid:xxx--

Environment Setup

Let’s set up the environment for working with MTOM:

# Start required applications
{:ok, _} = Application.ensure_all_started(:lather)

# Start Finch if not already running (safe to re-run this cell)
if Process.whereis(Lather.Finch) == nil do
  {:ok, _} = Supervisor.start_link([{Finch, name: Lather.Finch}], strategy: :one_for_one)
end

# Import MTOM modules for convenience
alias Lather.Mtom.{Attachment, Builder, Mime}

IO.puts("MTOM environment ready!")
IO.puts("")
IO.puts("Available modules:")
IO.puts("  - Lather.Mtom.Attachment: Create and manage attachments")
IO.puts("  - Lather.Mtom.Builder: Build MTOM messages")
IO.puts("  - Lather.Mtom.Mime: Handle MIME multipart operations")

Creating Attachments

The Lather.Mtom.Attachment module provides utilities for creating attachment structures that can be included in MTOM messages.

Basic Attachment Creation

Use Attachment.new/3 to create an attachment from binary data:

# Create sample binary data (simulating a file)
pdf_data = """
%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 4
trailer << /Size 4 /Root 1 0 R >>
startxref
%%EOF
"""

# Create an attachment
attachment = Attachment.new(pdf_data, "application/pdf")

IO.puts("Attachment Created:")
IO.puts("  ID: #{attachment.id}")
IO.puts("  Content-ID: #{attachment.content_id}")
IO.puts("  Content-Type: #{attachment.content_type}")
IO.puts("  Size: #{attachment.size} bytes")
IO.puts("  Transfer-Encoding: #{attachment.content_transfer_encoding}")

Attachment Options

You can customize attachments with various options:

# Create attachment with custom Content-ID
image_data = :crypto.strong_rand_bytes(1024)  # Simulated image data

attachment_with_options = Attachment.new(
  image_data,
  "image/jpeg",
  content_id: "company-logo@myapp.example.com",
  content_transfer_encoding: "binary"
)

IO.puts("Customized Attachment:")
IO.puts("  Content-ID: #{attachment_with_options.content_id}")
IO.puts("  Size: #{attachment_with_options.size} bytes")

# Generate Content-ID header format
content_id_header = Attachment.content_id_header(attachment_with_options)
IO.puts("  Content-ID Header: #{content_id_header}")

# Generate CID reference for XOP includes
cid_ref = Attachment.cid_reference(attachment_with_options)
IO.puts("  CID Reference: #{cid_ref}")

Creating Attachments from Files

The from_file/2 function reads a file and creates an attachment with automatic content type detection:

# Demonstrate file-based attachment creation
defmodule FileAttachmentDemo do
  def demo_from_file do
    # Create a temporary file for demonstration
    temp_path = Path.join(System.tmp_dir!(), "demo_document.pdf")

    sample_content = """
    %PDF-1.4
    Sample PDF content for demonstration purposes.
    This simulates a real PDF file structure.
    %%EOF
    """

    # Write the sample file
    File.write!(temp_path, sample_content)

    IO.puts("Creating attachment from file: #{temp_path}")

    case Attachment.from_file(temp_path) do
      {:ok, attachment} ->
        IO.puts("  Successfully created attachment!")
        IO.puts("  Detected content type: #{attachment.content_type}")
        IO.puts("  File size: #{attachment.size} bytes")
        IO.puts("  Content-ID: #{attachment.content_id}")

        # Clean up
        File.rm!(temp_path)
        {:ok, attachment}

      {:error, reason} ->
        IO.puts("  Error: #{inspect(reason)}")
        File.rm(temp_path)
        {:error, reason}
    end
  end

  def demo_content_type_detection do
    IO.puts("\nContent Type Detection by Extension:")

    extensions = [
      {".pdf", "application/pdf"},
      {".jpg", "image/jpeg"},
      {".png", "image/png"},
      {".doc", "application/msword"},
      {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
      {".zip", "application/zip"},
      {".json", "application/json"},
      {".xml", "application/xml"},
      {".txt", "text/plain"},
      {".csv", "text/csv"}
    ]

    Enum.each(extensions, fn {ext, expected_type} ->
      IO.puts("  #{ext} -> #{expected_type}")
    end)
  end
end

FileAttachmentDemo.demo_from_file()
FileAttachmentDemo.demo_content_type_detection()

XOP Include Elements

When an attachment is referenced in a SOAP message, it uses an XOP Include element. The xop_include/1 function generates this structure:

# Create an attachment
document_data = "Sample document content for XOP demonstration"
attachment = Attachment.new(document_data, "text/plain")

# Generate XOP Include element
xop_include = Attachment.xop_include(attachment)

IO.puts("XOP Include Structure:")
IO.inspect(xop_include, pretty: true)

IO.puts("\nThis structure will be serialized as:")
IO.puts("")

Interactive Attachment Creator

Let’s create an interactive widget to explore attachment creation:

# Create input widgets
# Fill in these fields, then run the attachment creator cell below
content_input = Kino.Input.textarea("Binary Content (text simulation)", default: "Hello, this is sample content!")
content_type_select = Kino.Input.select("Content Type", [
  {"application/pdf", "application/pdf"},
  {"image/jpeg", "image/jpeg"},
  {"image/png", "image/png"},
  {"text/plain", "text/plain"},
  {"application/xml", "application/xml"},
  {"application/json", "application/json"},
  {"application/octet-stream", "application/octet-stream"}
])
custom_id_input = Kino.Input.text("Custom Content-ID (optional)", default: "")

Kino.Layout.grid([
  content_input,
  content_type_select, custom_id_input
], columns: 2)
# Create attachment from inputs (re-run this cell after changing values above)
content = Kino.Input.read(content_input)
content_type = Kino.Input.read(content_type_select)
custom_id = Kino.Input.read(custom_id_input)

options = if custom_id != "" do
  [content_id: custom_id]
else
  []
end

IO.puts("Creating attachment...")
IO.puts(String.duplicate("=", 50))

attachment = Attachment.new(content, content_type, options)

IO.puts("Attachment Details:")
IO.puts("  ID: #{attachment.id}")
IO.puts("  Content-ID: #{attachment.content_id}")
IO.puts("  Content-Type: #{attachment.content_type}")
IO.puts("  Size: #{attachment.size} bytes")
IO.puts("  Encoding: #{attachment.content_transfer_encoding}")
IO.puts("")
IO.puts("Header Format: #{Attachment.content_id_header(attachment)}")
IO.puts("CID Reference: #{Attachment.cid_reference(attachment)}")
IO.puts("")
IO.puts("XOP Include:")
IO.inspect(Attachment.xop_include(attachment), pretty: true)
IO.puts(String.duplicate("=", 50))

Building MTOM Messages

The Lather.Mtom.Builder module handles the construction of complete MTOM messages by processing parameters, extracting attachments, and building the multipart structure.

Using Attachment Tuples in Parameters

The simplest way to include attachments is using the tuple format {:attachment, data, content_type}:

# Define parameters with an embedded attachment
params = %{
  "documentName" => "quarterly_report.pdf",
  "documentType" => "financial",
  "document" => {:attachment, "PDF content here...", "application/pdf"},
  "metadata" => %{
    "author" => "John Doe",
    "department" => "Finance"
  }
}

# Check if parameters contain attachments
has_attachments = Builder.has_attachments?(params)
IO.puts("Parameters contain attachments: #{has_attachments}")

# Process parameters to extract attachments
case Builder.process_parameters(params) do
  {:ok, {processed_params, attachments}} ->
    IO.puts("\nProcessed Parameters:")
    IO.inspect(processed_params, pretty: true)

    IO.puts("\nExtracted Attachments: #{length(attachments)}")
    Enum.each(attachments, fn att ->
      IO.puts("  - #{att.content_type} (#{att.size} bytes)")
    end)

  {:error, error} ->
    IO.puts("Error: #{inspect(error)}")
end

Building Complete MTOM Messages

Use build_mtom_message/3 to create a complete MTOM multipart message:

# Create a realistic upload scenario
pdf_content = String.duplicate("X", 5000)  # Simulated 5KB PDF

params = %{
  "UploadRequest" => %{
    "fileName" => "report.pdf",
    "fileSize" => byte_size(pdf_content),
    "uploadedBy" => "system",
    "file" => {:attachment, pdf_content, "application/pdf"}
  }
}

case Builder.build_mtom_message(:UploadDocument, params, namespace: "http://example.com/docs") do
  {:ok, {content_type, body}} ->
    IO.puts("MTOM Message Built Successfully!")
    IO.puts("")
    IO.puts("Content-Type Header:")
    IO.puts("  #{content_type}")
    IO.puts("")
    IO.puts("Message Size: #{byte_size(body)} bytes")
    IO.puts("")
    IO.puts("Message Preview (first 800 characters):")
    IO.puts(String.duplicate("-", 60))
    IO.puts(String.slice(body, 0, 800))
    IO.puts("...")
    IO.puts(String.duplicate("-", 60))

  {:error, error} ->
    IO.puts("Error building MTOM message: #{inspect(error)}")
end

Multiple Attachments

You can include multiple attachments in a single request:

# Create multiple attachment scenario
image1_data = :crypto.strong_rand_bytes(2048)
image2_data = :crypto.strong_rand_bytes(3072)
pdf_data = String.duplicate("PDF", 1000)

params = %{
  "BatchUpload" => %{
    "batchId" => "BATCH-001",
    "files" => [
      {:attachment, image1_data, "image/jpeg", content_id: "image1@batch"},
      {:attachment, image2_data, "image/png", content_id: "image2@batch"},
      {:attachment, pdf_data, "application/pdf", content_id: "doc@batch"}
    ],
    "metadata" => %{
      "totalFiles" => 3,
      "submittedAt" => DateTime.utc_now() |> DateTime.to_iso8601()
    }
  }
}

IO.puts("Building batch upload with multiple attachments...")

case Builder.build_mtom_message(:BatchUploadFiles, params, namespace: "http://example.com/batch") do
  {:ok, {content_type, body}} ->
    IO.puts("Batch MTOM Message Built!")
    IO.puts("")
    IO.puts("Content-Type: #{content_type}")
    IO.puts("Total Size: #{byte_size(body)} bytes")

    # Count MIME parts
    parts = String.split(body, "--uuid:")
    IO.puts("MIME Parts: #{length(parts) - 1}")

  {:error, error} ->
    IO.puts("Error: #{inspect(error)}")
end

Estimating Message Size

Before building a message, you can estimate its size:

# Create test parameters with various attachment sizes
test_cases = [
  {"Small file (1KB)", %{"file" => {:attachment, String.duplicate("x", 1024), "text/plain"}}},
  {"Medium file (50KB)", %{"file" => {:attachment, String.duplicate("x", 51200), "application/pdf"}}},
  {"Large file (500KB)", %{"file" => {:attachment, String.duplicate("x", 512000), "image/jpeg"}}},
  {"Multiple files", %{
    "files" => [
      {:attachment, String.duplicate("x", 10240), "image/jpeg"},
      {:attachment, String.duplicate("x", 20480), "image/png"},
      {:attachment, String.duplicate("x", 5120), "application/pdf"}
    ]
  }}
]

IO.puts("Message Size Estimation:")
IO.puts(String.duplicate("=", 50))

Enum.each(test_cases, fn {description, params} ->
  estimated_size = Builder.estimate_message_size(params)

  # Also build actual message to compare
  {:ok, {_, actual_body}} = Builder.build_mtom_message(:Test, params, namespace: "http://test")
  actual_size = byte_size(actual_body)

  accuracy = Float.round(estimated_size / actual_size * 100, 1)

  IO.puts("#{description}:")
  IO.puts("  Estimated: #{format_bytes(estimated_size)}")
  IO.puts("  Actual:    #{format_bytes(actual_size)}")
  IO.puts("  Accuracy:  #{accuracy}%")
  IO.puts("")
end)

defp format_bytes(bytes) when bytes >= 1024 * 1024 do
  "#{Float.round(bytes / 1024 / 1024, 2)} MB"
end
defp format_bytes(bytes) when bytes >= 1024 do
  "#{Float.round(bytes / 1024, 2)} KB"
end
defp format_bytes(bytes), do: "#{bytes} bytes"

MIME Multipart Handling

The Lather.Mtom.Mime module provides low-level utilities for building and parsing multipart/related MIME messages.

Understanding Multipart Structure

# Generate a boundary for our MIME message
boundary = Mime.generate_boundary()
IO.puts("Generated Boundary: #{boundary}")

# Build a Content-Type header
content_type_header = Mime.build_content_type_header(
  boundary,
  "application/xop+xml",
  "root@soap.example"
)

IO.puts("\nContent-Type Header:")
IO.puts(content_type_header)

# Parse it back
{:ok, extracted_boundary} = Mime.extract_boundary(content_type_header)
IO.puts("\nExtracted Boundary: #{extracted_boundary}")
IO.puts("Match: #{boundary == extracted_boundary}")

Building Multipart Messages Directly

You can use Mime.build_multipart_message/3 for lower-level control:

# Create SOAP envelope and attachments separately
soap_envelope = """
1.0UTF-8

  
    
      report.pdf
      
        
      
    
  

"""

# Create attachment
file_content = "This is the file content that would normally be binary PDF data."
attachment = Attachment.new(file_content, "application/pdf", content_id: "attachment1@lather.soap")

# Build multipart message
{content_type, multipart_body} = Mime.build_multipart_message(
  soap_envelope,
  [attachment],
  soap_content_type: "application/xop+xml",
  soap_charset: "UTF-8"
)

IO.puts("Built Multipart Message")
IO.puts(String.duplicate("=", 60))
IO.puts("Content-Type: #{content_type}")
IO.puts(String.duplicate("=", 60))
IO.puts(multipart_body)
IO.puts(String.duplicate("=", 60))

Parsing MTOM Responses

When receiving MTOM responses, use parse_multipart_message/3:

# Simulate an MTOM response
response_content_type = "multipart/related; boundary=\"response-boundary-123\"; type=\"application/xop+xml\"; start=\"\""

response_body = """
--response-boundary-123\r
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"\r
Content-Transfer-Encoding: 8bit\r
Content-ID: \r
\r
1.0UTF-8

  
    
      success
      
        
      
    
  

\r
--response-boundary-123\r
Content-Type: application/pdf\r
Content-Transfer-Encoding: binary\r
Content-ID: \r
\r
%PDF-1.4 Binary PDF content here...\r
--response-boundary-123--\r
"""

case Mime.parse_multipart_message(response_content_type, response_body) do
  {:ok, {soap_part, attachments}} ->
    IO.puts("Parsed MTOM Response Successfully!")
    IO.puts("")
    IO.puts("SOAP Part (first 300 chars):")
    IO.puts(String.slice(soap_part, 0, 300))
    IO.puts("...")
    IO.puts("")
    IO.puts("Attachments: #{length(attachments)}")
    Enum.each(attachments, fn att ->
      content_type = Map.get(att.headers, "content-type", "unknown")
      content_id = Map.get(att.headers, "content-id", "unknown")
      IO.puts("  - Type: #{content_type}")
      IO.puts("    ID: #{content_id}")
      IO.puts("    Size: #{byte_size(att.content)} bytes")
    end)

  {:error, reason} ->
    IO.puts("Parse Error: #{inspect(reason)}")
end

Validating Content-Type Headers

# Test various Content-Type headers
test_headers = [
  {"multipart/related; boundary=\"uuid:123\"; type=\"application/xop+xml\"", :valid},
  {"multipart/related; boundary=simple-boundary", :valid},
  {"application/soap+xml", :invalid},
  {"multipart/mixed; boundary=\"xxx\"", :invalid},
  {"multipart/related; type=\"application/xop+xml\"", :missing_boundary}
]

IO.puts("Content-Type Validation:")
IO.puts(String.duplicate("=", 60))

Enum.each(test_headers, fn {header, expected} ->
  result = Mime.validate_content_type(header)
  status = case result do
    :ok -> "Valid"
    {:error, reason} -> "Invalid (#{reason})"
  end

  match = case {result, expected} do
    {:ok, :valid} -> "PASS"
    {{:error, _}, :invalid} -> "PASS"
    {{:error, :missing_boundary}, :missing_boundary} -> "PASS"
    _ -> "FAIL"
  end

  IO.puts("[#{match}] #{status}")
  IO.puts("  Header: #{String.slice(header, 0, 50)}...")
  IO.puts("")
end)

Practical Examples

Let’s walk through complete real-world scenarios.

Example 1: Document Upload Service

defmodule DocumentUploadExample do
  alias Lather.Mtom.{Attachment, Builder}

  def upload_document(file_path, metadata) do
    IO.puts("Document Upload Service Example")
    IO.puts(String.duplicate("=", 50))

    # Simulate reading file (in real scenario, use File.read!)
    file_content = "Simulated content of #{Path.basename(file_path)}"
    file_size = byte_size(file_content)

    # Detect content type from extension
    content_type = get_content_type(Path.extname(file_path))

    IO.puts("File: #{file_path}")
    IO.puts("Content-Type: #{content_type}")
    IO.puts("Size: #{file_size} bytes")

    # Build parameters with attachment
    params = %{
      "UploadDocumentRequest" => %{
        "document" => %{
          "fileName" => Path.basename(file_path),
          "fileSize" => file_size,
          "contentType" => content_type,
          "content" => {:attachment, file_content, content_type}
        },
        "metadata" => metadata,
        "options" => %{
          "overwrite" => false,
          "notifyOnComplete" => true
        }
      }
    }

    case Builder.build_mtom_message(:UploadDocument, params,
      namespace: "http://documents.example.com/upload"
    ) do
      {:ok, {content_type_header, body}} ->
        IO.puts("\nMTOM Message Ready!")
        IO.puts("Content-Type: #{content_type_header}")
        IO.puts("Body Size: #{byte_size(body)} bytes")

        # In a real scenario, you would send this via HTTP:
        # Finch.build(:post, endpoint, headers, body)
        # |> Finch.request(Lather.Finch)

        {:ok, %{content_type: content_type_header, body: body}}

      {:error, error} ->
        {:error, error}
    end
  end

  defp get_content_type(ext) do
    case String.downcase(ext) do
      ".pdf" -> "application/pdf"
      ".doc" -> "application/msword"
      ".docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      ".xls" -> "application/vnd.ms-excel"
      ".xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
      ".jpg" -> "image/jpeg"
      ".jpeg" -> "image/jpeg"
      ".png" -> "image/png"
      _ -> "application/octet-stream"
    end
  end
end

# Test the document upload
metadata = %{
  "department" => "Legal",
  "category" => "Contracts",
  "tags" => ["Q4", "2024", "review"],
  "uploadedBy" => "user@example.com"
}

DocumentUploadExample.upload_document("/path/to/contract.pdf", metadata)

Example 2: Image Gallery Upload

defmodule ImageGalleryExample do
  alias Lather.Mtom.Builder

  def upload_gallery(images, gallery_name) do
    IO.puts("Image Gallery Upload Example")
    IO.puts(String.duplicate("=", 50))

    # Convert image list to attachments
    file_attachments = Enum.with_index(images, 1)
    |> Enum.map(fn {{name, data, content_type}, index} ->
      {:attachment, data, content_type, content_id: "image#{index}@gallery"}
    end)

    params = %{
      "CreateGalleryRequest" => %{
        "galleryName" => gallery_name,
        "images" => file_attachments,
        "settings" => %{
          "visibility" => "private",
          "thumbnailSize" => "medium",
          "watermark" => false
        }
      }
    }

    IO.puts("Gallery: #{gallery_name}")
    IO.puts("Images: #{length(images)}")

    case Builder.build_mtom_message(:CreateGallery, params,
      namespace: "http://gallery.example.com"
    ) do
      {:ok, {content_type, body}} ->
        IO.puts("\nGallery upload message ready!")
        IO.puts("Total message size: #{byte_size(body)} bytes")
        {:ok, %{content_type: content_type, body: body}}

      {:error, error} ->
        {:error, error}
    end
  end
end

# Create sample images (simulated binary data)
sample_images = [
  {"sunset.jpg", :crypto.strong_rand_bytes(4096), "image/jpeg"},
  {"mountains.png", :crypto.strong_rand_bytes(3072), "image/png"},
  {"beach.jpg", :crypto.strong_rand_bytes(5120), "image/jpeg"},
  {"forest.png", :crypto.strong_rand_bytes(2048), "image/png"}
]

ImageGalleryExample.upload_gallery(sample_images, "Vacation Photos 2024")

Example 3: Handling Large File Uploads

defmodule LargeFileHandler do
  alias Lather.Mtom.Builder

  @chunk_size 1024 * 1024  # 1MB chunks

  def prepare_large_upload(file_path, total_size) do
    IO.puts("Large File Upload Handler")
    IO.puts(String.duplicate("=", 50))

    # Calculate chunks
    chunk_count = ceil(total_size / @chunk_size)

    IO.puts("File: #{file_path}")
    IO.puts("Total Size: #{format_size(total_size)}")
    IO.puts("Chunk Size: #{format_size(@chunk_size)}")
    IO.puts("Chunks: #{chunk_count}")

    # Estimate total message sizes
    total_estimated = Enum.map(1..chunk_count, fn chunk_num ->
      chunk_size = min(@chunk_size, total_size - (chunk_num - 1) * @chunk_size)
      chunk_data = String.duplicate("x", chunk_size)

      params = %{
        "chunk" => {:attachment, chunk_data, "application/octet-stream"}
      }

      Builder.estimate_message_size(params)
    end)
    |> Enum.sum()

    IO.puts("\nUpload Strategy:")
    IO.puts("  Estimated total transfer: #{format_size(total_estimated)}")
    IO.puts("  Overhead: #{format_size(total_estimated - total_size)} (#{Float.round((total_estimated - total_size) / total_size * 100, 1)}%)")

    # Generate upload plan
    upload_plan = Enum.map(1..chunk_count, fn chunk_num ->
      offset = (chunk_num - 1) * @chunk_size
      chunk_size = min(@chunk_size, total_size - offset)

      %{
        chunk_number: chunk_num,
        offset: offset,
        size: chunk_size,
        is_last: chunk_num == chunk_count
      }
    end)

    {:ok, upload_plan}
  end

  def upload_chunk(file_path, chunk_info, upload_id) do
    # Simulate reading chunk from file
    chunk_data = String.duplicate("x", chunk_info.size)

    params = %{
      "UploadChunk" => %{
        "uploadId" => upload_id,
        "chunkNumber" => chunk_info.chunk_number,
        "offset" => chunk_info.offset,
        "isLast" => chunk_info.is_last,
        "data" => {:attachment, chunk_data, "application/octet-stream"}
      }
    }

    case Builder.build_mtom_message(:UploadChunk, params,
      namespace: "http://upload.example.com"
    ) do
      {:ok, {content_type, body}} ->
        IO.puts("Chunk #{chunk_info.chunk_number}: #{format_size(byte_size(body))} prepared")
        {:ok, body}

      {:error, error} ->
        {:error, error}
    end
  end

  defp format_size(bytes) when bytes >= 1024 * 1024 do
    "#{Float.round(bytes / 1024 / 1024, 2)} MB"
  end
  defp format_size(bytes) when bytes >= 1024 do
    "#{Float.round(bytes / 1024, 2)} KB"
  end
  defp format_size(bytes), do: "#{bytes} bytes"
end

# Demonstrate large file handling
{:ok, upload_plan} = LargeFileHandler.prepare_large_upload("large_video.mp4", 5 * 1024 * 1024)

IO.puts("\nUpload Plan:")
Enum.each(upload_plan, fn chunk ->
  status = if chunk.is_last, do: " (FINAL)", else: ""
  IO.puts("  Chunk #{chunk.chunk_number}: offset=#{chunk.offset}, size=#{chunk.size}#{status}")
end)

# Upload first chunk as example
IO.puts("\nUploading first chunk...")
LargeFileHandler.upload_chunk("large_video.mp4", hd(upload_plan), "upload-12345")

Performance Considerations

Size Comparison: Base64 vs MTOM

defmodule PerformanceComparison do
  alias Lather.Mtom.Builder

  def compare_encoding_methods(data_size) do
    IO.puts("Encoding Comparison: #{format_size(data_size)} of binary data")
    IO.puts(String.duplicate("=", 60))

    # Generate test data
    test_data = :crypto.strong_rand_bytes(data_size)

    # Base64 encoding
    base64_start = System.monotonic_time(:microsecond)
    base64_encoded = Base.encode64(test_data)
    base64_time = System.monotonic_time(:microsecond) - base64_start
    base64_size = byte_size(base64_encoded)

    # MTOM (binary transfer)
    mtom_start = System.monotonic_time(:microsecond)
    params = %{"data" => {:attachment, test_data, "application/octet-stream"}}
    {:ok, {_, mtom_body}} = Builder.build_mtom_message(:Test, params, namespace: "http://test")
    mtom_time = System.monotonic_time(:microsecond) - mtom_start
    mtom_size = byte_size(mtom_body)

    # Results
    IO.puts("\n| Metric          | Base64        | MTOM          | Winner |")
    IO.puts("|-----------------|---------------|---------------|--------|")

    # Size comparison
    size_winner = if base64_size < mtom_size, do: "Base64", else: "MTOM"
    IO.puts("| Data Size       | #{pad_size(base64_size)} | #{pad_size(mtom_size)} | #{size_winner} |")

    # Size overhead
    base64_overhead = Float.round((base64_size - data_size) / data_size * 100, 1)
    mtom_overhead = Float.round((mtom_size - data_size) / data_size * 100, 1)
    overhead_winner = if base64_overhead < mtom_overhead, do: "Base64", else: "MTOM"
    IO.puts("| Size Overhead   | #{pad_num(base64_overhead)}%        | #{pad_num(mtom_overhead)}%        | #{overhead_winner} |")

    # Time comparison
    time_winner = if base64_time < mtom_time, do: "Base64", else: "MTOM"
    IO.puts("| Encoding Time   | #{pad_num(base64_time)} us      | #{pad_num(mtom_time)} us      | #{time_winner} |")

    %{
      base64: %{size: base64_size, time: base64_time, overhead: base64_overhead},
      mtom: %{size: mtom_size, time: mtom_time, overhead: mtom_overhead}
    }
  end

  defp format_size(bytes) when bytes >= 1024 * 1024 do
    "#{Float.round(bytes / 1024 / 1024, 2)} MB"
  end
  defp format_size(bytes) when bytes >= 1024 do
    "#{Float.round(bytes / 1024, 2)} KB"
  end
  defp format_size(bytes), do: "#{bytes} bytes"

  defp pad_size(bytes), do: format_size(bytes) |> String.pad_trailing(13)
  defp pad_num(num), do: to_string(num) |> String.pad_leading(6)
end

# Compare at different sizes
sizes = [1024, 10240, 102400, 1024000]

IO.puts("Performance Analysis at Different Sizes")
IO.puts(String.duplicate("=", 60))

Enum.each(sizes, fn size ->
  IO.puts("")
  PerformanceComparison.compare_encoding_methods(size)
end)

Memory Usage Analysis

defmodule MemoryAnalysis do
  def analyze_memory_usage(attachment_sizes) do
    IO.puts("Memory Usage Analysis")
    IO.puts(String.duplicate("=", 50))

    results = Enum.map(attachment_sizes, fn size ->
      # Measure memory before
      :erlang.garbage_collect()
      memory_before = :erlang.memory(:total)

      # Create attachment
      data = :crypto.strong_rand_bytes(size)
      attachment = Lather.Mtom.Attachment.new(data, "application/octet-stream")

      # Build message
      params = %{"file" => {:attachment, data, "application/octet-stream"}}
      {:ok, {_, body}} = Lather.Mtom.Builder.build_mtom_message(:Test, params, namespace: "http://test")

      # Measure memory after
      memory_after = :erlang.memory(:total)
      memory_used = memory_after - memory_before

      %{
        input_size: size,
        attachment_size: attachment.size,
        message_size: byte_size(body),
        memory_used: memory_used,
        memory_ratio: Float.round(memory_used / size, 2)
      }
    end)

    IO.puts("\n| Input Size | Message Size | Memory Used | Ratio |")
    IO.puts("|------------|--------------|-------------|-------|")

    Enum.each(results, fn r ->
      IO.puts("| #{format_size(r.input_size)} | #{format_size(r.message_size)} | #{format_size(r.memory_used)} | #{r.memory_ratio}x |")
    end)

    results
  end

  defp format_size(bytes) when bytes >= 1024 * 1024 do
    "#{Float.round(bytes / 1024 / 1024, 2)} MB" |> String.pad_trailing(10)
  end
  defp format_size(bytes) when bytes >= 1024 do
    "#{Float.round(bytes / 1024, 2)} KB" |> String.pad_trailing(10)
  end
  defp format_size(bytes), do: "#{bytes} B" |> String.pad_trailing(10)
end

# Analyze memory at different sizes
MemoryAnalysis.analyze_memory_usage([1024, 10240, 102400, 512000])

When MTOM is Most Beneficial

IO.puts("""
MTOM Performance Guidelines

BEST USE CASES:
  - Files > 100KB: 20-33% bandwidth savings
  - Binary data (images, PDFs): No base64 encoding overhead
  - High-frequency uploads: Reduced CPU from encoding
  - Memory-constrained systems: No base64 expansion

LESS BENEFICIAL:
  - Small files < 1KB: MIME overhead may exceed savings
  - Text-heavy XML: Base64 overhead is acceptable
  - Simple integrations: Base64 is simpler to debug

OPTIMIZATION TIPS:
  1. Batch small files together
  2. Use streaming for files > 10MB
  3. Consider compression before MTOM
  4. Set appropriate chunk sizes for large files
  5. Monitor memory usage with :erlang.memory/0

SIZE THRESHOLDS (Approximate):
  | File Size   | Recommendation           |
  |-------------|--------------------------|
  | < 1 KB      | Base64 (simpler)         |
  | 1-10 KB     | Either (MTOM slightly better) |
  | 10-100 KB   | MTOM (noticeable savings) |
  | 100 KB - 1 MB | MTOM (significant savings) |
  | > 1 MB      | MTOM + chunking          |
""")

Error Handling

Attachment Validation Errors

defmodule ErrorHandlingExamples do
  alias Lather.Mtom.{Attachment, Builder}

  def demonstrate_validation_errors do
    IO.puts("Attachment Validation Errors")
    IO.puts(String.duplicate("=", 50))

    test_cases = [
      {"Empty content type", fn ->
        Attachment.new("data", "")
      end},

      {"Invalid content type format", fn ->
        Attachment.new("data", "invalid")
      end},

      {"Empty data", fn ->
        attachment = Attachment.new("", "application/pdf", validate: false)
        Attachment.validate(attachment)
      end},

      {"Invalid encoding", fn ->
        Attachment.new("data", "application/pdf", content_transfer_encoding: "invalid-encoding")
      end}
    ]

    Enum.each(test_cases, fn {description, test_fn} ->
      IO.puts("\nTest: #{description}")
      try do
        result = test_fn.()
        case result do
          :ok -> IO.puts("  Result: :ok (unexpected)")
          {:error, reason} -> IO.puts("  Result: {:error, #{inspect(reason)}}")
          %Attachment{} -> IO.puts("  Result: Attachment created (unexpected)")
        end
      rescue
        e in ArgumentError ->
          IO.puts("  Raised: ArgumentError - #{e.message}")
        e ->
          IO.puts("  Raised: #{inspect(e)}")
      end
    end)
  end

  def demonstrate_builder_errors do
    IO.puts("\n\nBuilder Error Handling")
    IO.puts(String.duplicate("=", 50))

    test_cases = [
      {"Malformed attachment tuple", %{
        "file" => {:attachment, 123, "application/pdf"}  # data should be binary
      }},

      {"Missing content type", %{
        "file" => {:attachment, "data"}  # missing content type
      }},

      {"Invalid options type", %{
        "file" => {:attachment, "data", "application/pdf", "not-a-list"}
      }}
    ]

    Enum.each(test_cases, fn {description, params} ->
      IO.puts("\nTest: #{description}")

      case Builder.build_mtom_message(:Test, params, namespace: "http://test") do
        {:ok, _} ->
          IO.puts("  Result: Success (unexpected)")

        {:error, reason} ->
          IO.puts("  Result: {:error, #{inspect(reason)}}")
      end
    end)
  end

  def demonstrate_mime_errors do
    IO.puts("\n\nMIME Parsing Errors")
    IO.puts(String.duplicate("=", 50))

    test_cases = [
      {"Missing boundary", "multipart/related; type=\"application/xop+xml\""},
      {"Not multipart/related", "application/soap+xml"},
      {"Empty content-type", ""}
    ]

    Enum.each(test_cases, fn {description, content_type} ->
      IO.puts("\nTest: #{description}")

      result = Lather.Mtom.Mime.validate_content_type(content_type)
      IO.puts("  Input: #{inspect(content_type)}")
      IO.puts("  Result: #{inspect(result)}")
    end)
  end
end

ErrorHandlingExamples.demonstrate_validation_errors()
ErrorHandlingExamples.demonstrate_builder_errors()
ErrorHandlingExamples.demonstrate_mime_errors()

Handling Size Limits

defmodule SizeLimitDemo do
  @max_attachment_size 100 * 1024 * 1024  # 100MB default

  def check_size_limits(data_size) do
    IO.puts("Size Limit Check: #{format_size(data_size)}")

    cond do
      data_size > @max_attachment_size ->
        IO.puts("  Status: EXCEEDS LIMIT")
        IO.puts("  Max allowed: #{format_size(@max_attachment_size)}")
        IO.puts("  Suggestion: Use chunked upload or streaming")
        {:error, :attachment_too_large}

      data_size > @max_attachment_size * 0.8 ->
        IO.puts("  Status: WARNING - Near limit (80%+)")
        IO.puts("  Consider: Chunked upload for safety margin")
        {:ok, :warning}

      true ->
        IO.puts("  Status: OK")
        {:ok, :within_limits}
    end
  end

  defp format_size(bytes) when bytes >= 1024 * 1024 do
    "#{Float.round(bytes / 1024 / 1024, 2)} MB"
  end
  defp format_size(bytes) when bytes >= 1024 do
    "#{Float.round(bytes / 1024, 2)} KB"
  end
  defp format_size(bytes), do: "#{bytes} bytes"
end

# Test size limits
IO.puts("Size Limit Validation")
IO.puts(String.duplicate("=", 50))

[
  10 * 1024 * 1024,      # 10 MB
  50 * 1024 * 1024,      # 50 MB
  85 * 1024 * 1024,      # 85 MB (warning)
  150 * 1024 * 1024      # 150 MB (exceeds)
]
|> Enum.each(fn size ->
  SizeLimitDemo.check_size_limits(size)
  IO.puts("")
end)

IO.puts("""
Configuration Tip:
  Set custom max size in config/config.exs:

  config :lather, :max_attachment_size, 200 * 1024 * 1024  # 200MB
""")

Summary and Quick Reference

IO.puts("""
MTOM ATTACHMENTS - QUICK REFERENCE

CREATING ATTACHMENTS:
  # From binary data
  attachment = Attachment.new(data, "application/pdf")

  # With options
  attachment = Attachment.new(data, "image/jpeg",
    content_id: "custom-id@example.com"
  )

  # From file
  {:ok, attachment} = Attachment.from_file("/path/to/file.pdf")

BUILDING MTOM MESSAGES:
  # Using attachment tuples in parameters
  params = %{
    "file" => {:attachment, data, "application/pdf"}
  }

  {:ok, {content_type, body}} = Builder.build_mtom_message(
    :UploadFile,
    params,
    namespace: "http://example.com"
  )

MIME OPERATIONS:
  # Generate boundary
  boundary = Mime.generate_boundary()

  # Build Content-Type header
  header = Mime.build_content_type_header(boundary, type, start_id)

  # Parse MTOM response
  {:ok, {soap, attachments}} = Mime.parse_multipart_message(
    content_type, body
  )

ATTACHMENT UTILITIES:
  # Get Content-ID header format
  Attachment.content_id_header(att)  # ""

  # Get CID reference for XOP
  Attachment.cid_reference(att)  # "cid:id@lather.soap"

  # Generate XOP Include element
  Attachment.xop_include(att)

  # Check if value is attachment
  Attachment.is_attachment?(value)

  # Validate attachment
  Attachment.validate(att)

BUILDER UTILITIES:
  # Check for attachments in params
  Builder.has_attachments?(params)

  # Process and extract attachments
  {:ok, {processed, attachments}} = Builder.process_parameters(params)

  # Estimate message size
  size = Builder.estimate_message_size(params)

SUPPORTED CONTENT TYPES:
  Documents: application/pdf, application/msword, application/vnd.openxmlformats-*
  Images: image/jpeg, image/png, image/gif, image/tiff, image/bmp, image/webp
  Archives: application/zip, application/gzip, application/x-tar
  Text: text/plain, text/csv, text/xml, application/json
  Binary: application/octet-stream
  Media: video/mp4, audio/mpeg, audio/wav
""")

Next Steps

Congratulations! You now understand MTOM attachments in Lather. Here’s what to explore next:

  1. Enterprise Integration: See the enterprise_integration.livemd for complex multi-service scenarios
  2. Debugging: Use debugging_troubleshooting.livemd to diagnose MTOM issues
  3. SOAP Server Development: Learn to build MTOM-enabled SOAP servers
  4. Performance Tuning: Optimize large file transfers for production

Happy SOAP-ing with binary attachments!