Error in Writing File Copy

Hi all!

Trying to follow along with this and hitting an error I cant figure out. When we first get to testing %Plug.Upload{} in IEx after implementing the create_upload_from_plug_upload fn, I’m getting a function clause error.

If I understand the logs, apparently I’m giving a nil value to Upload.local_path(). I’m not sure how to rectify this, as I can’t figure out how I’m giving a nil value. Is my upload.id value nil for File.cp/2 ? I don’t understand how if so.

I’ve followed the tutorial up to this point without any issues and my code is identical. Thanks for any help!

Here is the full error -

<code>iex(3)> Documents.create_upload_from_plug_upload(upload) 
[debug] QUERY OK db=13.9ms idle=487.9ms
begin []
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
[debug] QUERY OK db=2.9ms
INSERT INTO "uploads" ("content_type","filename","hash","size","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["image/png", "phoenix.png", "07aa9b01595fe10fd4e5ceb6cc67ba186ef8ce91e5a5cba47166f7d8498d7852", 13900, ~N[2022-11-16 07:26:37], ~N[2022-11-16 07:26:37]]
↳ anonymous fn/4 in Woof.Documents.create_upload_from_plug_upload/1, at: lib/woof/documents.ex:25
[debug] QUERY OK db=0.3ms
rollback []
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
** (FunctionClauseError) no function clause matching in IO.chardata_to_string/1 

The following arguments were given to IO.chardata_to_string/1:

# 1
nil

Attempted function clauses (showing 2 out of 2):

def chardata_to_string(string) when is_binary(string)
def chardata_to_string(list) when is_list(list)

(elixir 1.13.4) IO.chardata_to_string/1
(elixir 1.13.4) lib/path.ex:538: Path.join/2
(elixir 1.13.4) lib/path.ex:509: Path.join/1
(woof 0.1.0) lib/woof/documents.ex:26: anonymous fn/4 in Woof.Documents.create_upload_from_plug_upload/1
(ecto_sql 3.9.0) lib/ecto/adapters/sql.ex:1190: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection 2.4.2) lib/db_connection.ex:1562: DBConnection.run_transaction/4</code>

And my relevant code-

lib.woof.documents.ex

defmodule Woof.Documents do
  import Ecto.Query, warn: false

  alias Woof.Repo
  alias Woof.Documents.Upload

  def create_upload_from_plug_upload(%Plug.Upload{
        filename: filename,
        path: tmp_path,
        content_type: content_type
      }) do
    hash =
      File.stream!(tmp_path, [], 2048)
      |> Upload.sha256()

    with {:ok, %File.Stat{size: size}} <- File.stat(tmp_path),
         {:ok, upload} <-
           %Upload{}
           |> Upload.changeset(%{
             filename: filename,
             content_type: content_type,
             hash: hash,
             size: size
           })
           |> Repo.insert(),
         :ok <-
           File.cp(
             tmp_path,
             Upload.local_path(upload.id, filename)
           ) do
      {:ok, upload}
    else
      {:error, reason} = error -> error
    end
  end
end

lib.woof.documents.upload.ex

defmodule Woof.Documents.Upload do
  use Ecto.Schema
  import Ecto.Changeset

  schema "uploads" do
    field :content_type, :string
    field :filename, :string
    field :hash, :string
    field :size, :integer

    timestamps()
  end

  @doc false
  def changeset(upload, attrs) do
    upload
    |> cast(attrs, [:filename, :size, :content_type, :hash])
    |> validate_required([:filename, :size, :content_type, :hash])
    |> validate_number(:size, greater_than: 0)
    |> validate_length(:hash, is: 64)
  end

  def sha256(chunks_enum) do
    chunks_enum
    |> Enum.reduce(
      :crypto.hash_init(:sha256),
      &:crypto.hash_update(&2, &1)
    )
    |> :crypto.hash_final()
    |> Base.encode16()
    |> String.downcase()
  end

  def upload_directory do
    Application.get_env(:woof, :uploads_directory)
  end

  def local_path(id, filename) do
    [upload_directory(), "#{id}-#{filename}"]
    |> Path.join()
  end
end

The error you’re getting is what I’d expect if upload_directory() returned nil - if id or filename was nil, the "#{id}-#{filename}" part wouldn’t let those values get to Path.join.

iex(1)> Path.join(nil, "foo")
** (FunctionClauseError) no function clause matching in IO.chardata_to_string/1    
    
    The following arguments were given to IO.chardata_to_string/1:
    
        # 1
        nil
    
    Attempted function clauses (showing 2 out of 2):
    
        def chardata_to_string(string) when is_binary(string)
        def chardata_to_string(list) when is_list(list)
    
    (elixir 1.14.0) lib/io.ex:670: IO.chardata_to_string/1
    (elixir 1.14.0) lib/path.ex:548: Path.join/2
    iex:1: (file)

I definitely track with that. I can successfully write the first upload with the front end, writing to the temporary uploads directory. So I clearly have a filename, but not an id. Don’t know why I’m missing that id. :thinking:

[debug] Processing with WoofWeb.UploadController.create/2
  Parameters: %{"_csrf_token" => "IRcuIzFFeS81JQZYORMQPjIMbWAGC2AhnNTEarLnAiu7QgfnSnXPtnUf", "upload" => %Plug.Upload{content_type: "image/jpeg", filename: "hound.jpg", path: "/var/folders/w_/rrq1p7rj7pq7js2brwm6wpsc0000gn/T/plug-1668/multipart-1668625495-142439731089-6"}}
  Pipelines: [:browser]
UPLOAD: %Plug.Upload{
  content_type: "image/jpeg",
  filename: "hound.jpg",
  path: "/var/folders/w_/rrq1p7rj7pq7js2brwm6wpsc0000gn/T/plug-1668/multipart-1668625495-142439731089-6"
}
[info] Sent 200 in 26ms