Search code examples
pdfuploadelixirpdf-generationphoenix-framework

Elixir error when trying to upload pdf from pdf generator


I'm trying to upload and create a download prompt for a generated pdf via the store function but I keep getting an error as if a pdf hasn't been generated but I can confirm that there is a pdf file that is being saved locally.

This is my pdf handler:

defp handle_export_pdf(template_name, report_name, data) do
    filename = "#{report_name}.pdf"

    tmp_dir = "#{System.tmp_dir()}"
    dir_path = "#{tmp_dir}/#{filename}"

    with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)},
         {:write_file, :ok} <-
           {:write_file, generate_pdf(data, report_name, dir_path)},
         {:store_on_cloud, {:ok, file}} <-
           {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})},
         {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do
      {:ok, file}
    else
      {:create_dir, _error} ->
        {:error, "Error creating directory"}

      {:write_file, _error} ->
        {:error, "Error writing data to file"}

      {:store_on_cloud, _error} ->
        {:error, "Error uploading file"}

      {:delete_file, _error} ->
        {:error, "Error deleting file"}
    end
  end

The error happens when trying to store as I get the error "Error uploading file", along with this:

[error] GenServer #PID<0.143080.0> terminating ** (Plug.Conn.NotSentError) a response was neither set nor sent from the connection

This is my generate_pdf function that handles the generation of pdfs:

def generate_pdf(data, title, path) do
    html =
      Sneeze.render([
        :html,
        [
          :body,
          %{
            style:
              style(%{
                "font-family" => "Helvetica",
                "font-size" => "20pt"
              })
          },
          render_header(title),
          render_table(data)
        ]
      ])

    {:ok, filename} = PdfGenerator.generate(html, page_size: "A3", shell_params: ["--dpi", "300"])

    File.rename(filename, path)

    :ok
  end

Here's my full controller code. There's kind of a lot going on as I'll refactor this when I get it working. But this also includes a xlsx handler which uploads and prompts a download just fine. The issue only exists with pdf for some reason.

defmodule EpmsWeb.ReportExportController do
  use EpmsWeb, :controller

  alias Epms.Delivery
  alias Epms.Assets
  alias Epms.Repo

  alias Epms.HandOffFileUploader
  alias Elixlsx.Workbook
  alias Elixlsx.Sheet

  @report_names %{
    "a" => "Title A",
    "b" => "Title B",
    "c" => "Title C",
  }

  def export_report(conn, %{
        "file_type" => file_type,
        "frequency_type" => frequency_type,
        "from_date" => from_date,
        "to_date" => to_date,
        "template_name" => template_name
      }) do
    report_name = @report_names[template_name]

    assigns = fetch_report_data(conn, template_name, file_type)

    filename_result =
      case file_type do
        "xlsx" ->
          handle_export_xlsx(template_name, report_name, assigns)

        "pdf" ->
          handle_export_pdf(template_name, report_name, assigns)

        _ ->
          {:error, :unsupported_file_type}
      end

    case filename_result do
      {:ok, filename} ->
        conn
        |> redirect(external: HandOffFileUploader.url(filename, signed: true))
        |> halt()

      _ ->
        conn
        |> put_flash(:error, "Unsupported file type or error in generating report.")
        |> halt()
    end
  end

  defp fetch_report_data(conn, template_name, file_type) do
    case {template_name, file_type} do

      {"a", "xlsx"} ->
        Epms.ExportsHelper.a.get_rows(conn)

      {"b", "xlsx"} ->
        Epms.ExportsHelper.b.get_rows(conn)

      {"c", "xlsx"} ->
        Epms.ExportsHelper.c.get_rows(conn)
      {"a", "pdf"} ->
        Epms.ExportsHelper.a.get_rows(conn)

      {"b", "pdf"} ->
        Epms.ExportsHelper.b.get_rows(conn)

      {"c", "pdf"} ->
        Epms.ExportsHelper.c.get_rows(conn)

      # Add more cases for different file types as needed
      _ ->
        %{}
    end
  end

  defp handle_export_xlsx(template_name, report_name, data) do
    # Use the filename_for function to generate a filename
    filename = "#{report_name}.xlsx"

    tmp_dir = "#{System.tmp_dir()}"
    dir_path = "#{tmp_dir}/#{filename}"

    with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)},
         {:write_file, :ok} <-
           {:write_file, write_data_to_file(template_name, dir_path, data)},
         {:store_on_cloud, {:ok, file}} <-
           {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})},
         {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do
      {:ok, file}
    else
      {:create_dir, _error} ->
        {:error, "Error creating directory"}

      {:write_file, _error} ->
        {:error, "Error writing data to file"}

      {:store_on_cloud, _error} ->
        {:error, "Error uploading file"}

      {:delete_file, _error} ->
        {:error, "Error deleting file"}
    end
  end

  defp write_data_to_file(report_template, path, data) do
    report_codes = Enum.map(@report_names, fn {code, _name} -> code end)

    if report_template in report_codes do
      write_xlsx(path, data)
    end
  end

  defp handle_export_pdf(template_name, report_name, data) do
    filename = "#{report_name}.pdf"

    tmp_dir = "#{System.tmp_dir()}"
    dir_path = "#{tmp_dir}/#{filename}"

    with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)},
         {:write_file, :ok} <-
           {:write_file, generate_pdf(data, report_name, dir_path)},
         {:store_on_cloud, {:ok, file}} <-
           {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})},
         {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do
      {:ok, file}
    else
      {:create_dir, _error} ->
        {:error, "Error creating directory"}

      {:write_file, _error} ->
        {:error, "Error writing data to file"}

      {:store_on_cloud, _error} ->
        {:error, "Error uploading file"}

      {:delete_file, _error} ->
        {:error, "Error deleting file"}
    end
  end

  defp write_xlsx(path, rows) do
    # Assuming `rows` is a list of lists, where each inner list represents a row in the sheet
    max_columns = Enum.max_by(rows, &length/1) |> length
    # Generate a map with the same width for all columns
    uniform_col_widths = 1..max_columns |> Enum.map(&{&1, 20}) |> Enum.into(%{})

    # Write the xlsx file
    sheet1 = %Sheet{name: "Export", rows: rows, col_widths: uniform_col_widths}
    workbook = %Workbook{sheets: [sheet1]}

    workbook
    |> Elixlsx.write_to(path)

    :ok
  end

  defp format_date_to_string(date) when is_nil(date), do: ""
  defp format_date_to_string(%Date{} = date), do: Date.to_string(date)





  # PDF GENERATION

  def generate_pdf(data, title, path) do
    html =
      Sneeze.render([
        :html,
        [
          :body,
          %{
            style:
              style(%{
                "font-family" => "Helvetica",
                "font-size" => "20pt"
              })
          },
          render_header(title),
          render_table(data)
        ]
      ])

    {:ok, filename} = PdfGenerator.generate(html, page_size: "A3", shell_params: ["--dpi", "300"])

    File.rename(filename, path)

    :ok
  end

  defp style(style_map) do
    style_map
    |> Enum.map(fn {key, value} ->
      "#{key}: #{value}"
    end)
    |> Enum.join(";")
  end

  defp render_header(title) do

    date = DateTime.utc_now()
    date_string = "#{date.year}/#{date.month}/#{date.day}"

    [
      :div,
      %{
        style:
          style(%{
            "display" => "flex",
            "flex-direction" => "column",
            "align-items" => "flex-start",
          })
      },
      [
        :div,
        %{
          style:
            style(%{
              "display" => "inline-block",
              "margin-top" => "10pt"
            })
        },
        [
          :h1,
          %{
            style:
              style(%{
                "font-size" => "16pt",
                "margin-top" => "0pt",
                "padding-top" => "0pt"
              })
          },
          title
        ],
        [
          :h3,
          %{
            style:
              style(%{
                "font-size" => "14pt",
              })
          },
          date_string
        ]
      ]
    ]
  end

  defp render_table(data) do
    table = [
      :table,
      %{
        style:
          style(%{
            "border" => "1px solid black",
            "border-collapse" => "collapse",
            "width" => "100%"
          })
      },
  ]
    rows = Enum.map(data, &render_row/1)
    table ++ rows
  end

  defp render_row(data) do
    row = [
      :tr,
      %{
        style:
          style(%{
            "border" => "1px solid black",
            "border-collapse" => "collapse"
          })
      },
    ]

    items = Enum.map(data, &render_items/1)
    row ++ items
  end

  defp render_items(item) do
    [
      :td,
      %{
        style:
          style(%{
            "border" => "1px solid black",
            "border-collapse" => "collapse",
            "padding" => "5pt"
          })
      },
      [
        :span,
        %{
          style:
            style(%{
              "font-size" => "12pt",
              "margin-top" => "0pt",
              "padding-top" => "0pt"
            })
        },
        item
      ],
    ]
  end
end


Solution

  • Fixed the issue which was actually in my uploader file. I had to add .pdf to the list of whitelisted file extensions.

    def validate({file, _}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    
    case Enum.member?(~w(.txt .csv .xlsx .xls .pdf), file_extension) do
      true -> :ok
      false -> {:error, "invalid file type"}
    end
    

    end