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
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