Hi. I am busy building my first Phoenix web app but I got stuck with the following problem: my context functions seem to be getting enormous and I do not have the functional programming experience or knowledge to refactor and test them. The particular code that I am struggling with is the functions involved in uploading files to the website. Here is the relevant functions:
def upload_file(%Plug.Upload{
filename: filename,
path: tmp_path,
content_type: mime
}) do
Repo.transaction fn ->
with hash <- hash_file(tmp_path),
{:ok, storename} <- generate_file_name(hash, mime),
{:ok, location} <- filestore().store(
:uploads,
tmp_path,
storename
),
{:ok, size} <- file_size(tmp_path),
{:ok, file} <- create_file(%{
name: filename,
content_type: mime,
size: size,
location: location,
hash: hash
}) do
file #transaction returns {:ok, _} already
else
{:error, reason} -> Repo.rollback(reason)
end
end
end
defp filestore() do
Application.get_env(:boomhuis, :filestore)
|> Map.get(:cabinet, Filestore.Local)
end
defp hash_file(path) do
File.stream!(path, [], 2048)
|> Enum.reduce(
:crypto.hash_init(:sha256),
&(:crypto.hash_update(&2, &1))
)
|> :crypto.hash_final()
|> Base.encode16()
|> String.downcase()
end
defp generate_file_name(hash, content_type) do
case MIME.valid?(content_type) do
true ->
extension =
content_type
|> MIME.extensions()
|> Enum.at(0)
{:ok, hash <> "." <> extension}
false ->
{:error, :unsupported}
end
end
defp file_size(path) do
case FUtils.stat(path) do
{:ok, %FUtils.Stat{size: size}} ->
{:ok, size}
other ->
other
end
end
I have a few concerns / questions:
1.) As this function is clearly doing many things, how can I refactor this code to make it more succinct and clean? Obviously I want to split it into multiple function clauses, but I do not know where to begin or how to do it.
2.) How should I test this function as it relies on a lot of different functionalities? In particular, how does one test Filestore part as it interacts with the filesystem?
3.) Is one of the problems of the testing difficulty the fact that the bottom functions are private? Should I make them public or not?
4.) How would you guys (the experts / better versed programmers) implement this?
Any advice, examples and resources are appreciated. Please excuse my questions as I am still a beginner Elixir programmer/hobbyist.