2

There's a bunch of information on how to do this with Phoenix, but I'm purposefully avoiding using Phoenix until I learn more about how Elixir works.

To that end, I have the following Plug.Router path:

defmodule ElixirHttpServer do
  use Plug.Router
  use Plug.ErrorHandler

  plug(Plug.Parsers, parsers: [:urlencoded, {:multipart, length: 1_000_000_000}])
  plug(Plug.Logger)
  plug(:match)
  plug(:dispatch)


  post "/upload" do
    IO.inspect(Plug.Conn.read_body(conn), label: "body")
    send_resp(conn, 201, "Uploaded")
  end
end

That accepts a file upload from a form, rendered in an EEx template:

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file">
  <input type="submit">
</form>

When uploading a file via this form, I get the following output from IO.inspect(Plug.Conn.read_body(conn)):

18:11:13.097 [info]  POST /upload                                                                                                                                                                                                                                                [205/3062]
body: {:ok, "",
 %Plug.Conn{
   adapter: {Plug.Cowboy.Conn, :...},
   assigns: %{},
   before_send: [#Function<1.128679493/1 in Plug.Logger.call/2>],
   body_params: %{},
   cookies: %Plug.Conn.Unfetched{aspect: :cookies},
   halted: false,
   host: "localhost",
   method: "POST",
   owner: #PID<0.750.0>,
   params: %{},
   path_info: ["upload"],
   path_params: %{},
   port: 8080,
   private: %{
     plug_multipart: :done,
     plug_route: {"/upload",
      #Function<1.2199674/2 in ElixirHttpServer.do_match/4>}
   },
   query_params: %{},
   query_string: "",
   remote_ip: {127, 0, 0, 1},
   req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
   req_headers: [
     {"accept",
      "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"},
     {"accept-encoding", "gzip, deflate, br"},
     {"accept-language", "en-US,en;q=0.9"},
     {"cache-control", "no-cache"},
     {"connection", "keep-alive"},
     {"content-length", "44"},
     {"content-type",
      "multipart/form-data; boundary=----WebKitFormBoundary4wTVqggydpkBg30n"},
     {"host", "localhost:8080"},
     {"origin", "http://localhost:8080"},
     {"pragma", "no-cache"},
     {"referer", "http://localhost:8080/"},
     {"sec-ch-ua",
      "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\""},
     {"sec-ch-ua-mobile", "?0"},
     {"sec-fetch-dest", "document"},
     {"sec-fetch-mode", "navigate"},
     {"sec-fetch-site", "same-origin"},
     {"sec-fetch-user", "?1"},
     {"upgrade-insecure-requests", "1"},
     {"user-agent",
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"}
   ],
   request_path: "/upload",
   resp_body: nil,
   resp_cookies: %{},
   resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
   scheme: :http,
   script_name: [],
   secret_key_base: nil,
   state: :unset,
   status: nil
 }}

18:11:13.099 [info]  Sent 201 in 2ms

I've read through the Plug.Upload docs several times, but it appears to be primarily a struct that you can use.

The Plug.Parsers documentation says the following, but I don't know what "starting the :plug application" actually means:

File handling

If a file is uploaded via any of the parsers, Plug will stream the uploaded contents to a file in a temporary directory in order to avoid loading the whole file into memory. For such, the :plug application needs to be started in order for file uploads to work. More details on how the uploaded file is handled can be found in the documentation for Plug.Upload.

When a file is uploaded, the request parameter that identifies that file will be a Plug.Upload struct with information about the uploaded file (e.g. filename and content type) and about where the file is stored.

I added :plug to my extra_applications, but this did not seem to change anything:

  def application do
    [
      extra_applications: [:plug, :plug_cowboy, :logger],
      mod: {ElixirHttpServer.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:plug_cowboy, "~> 2.4"},
      {:hackney, "~> 1.17.0"},
      {:ex_aws, "~> 2.1"},
      {:ex_aws_s3, "~> 2.0"},
      {:configparser_ex, "~> 4.0"},
      {:sweet_xml, "~> 0.6"}
    ]
  end
end

For reference, this is my supervisor application:

defmodule ElixirHttpServer.Application do
  @moduledoc "OTP application for S3 bucket list/upload"

  use Application
  require Logger

  def start(_type, _args) do
    children = [
      {Plug.Cowboy, scheme: :http, plug: ElixirHttpServer, options: [port: cowboy_port()]}
    ]

    opts = [strategy: :one_for_one, name: ElixirHttpServer.Supervisor]

    Logger.info("Starting the application...")
    Supervisor.start_link(children, opts)
  end

  defp cowboy_port(), do: Application.get_env(:elixir_http_server, :cowboy_port, 8080)
end
diplosaurus
  • 2,538
  • 5
  • 25
  • 53

1 Answers1

1

I was missing the name attribute of my input type="file" element.

<input type="file" name="file"> did the trick.

diplosaurus
  • 2,538
  • 5
  • 25
  • 53