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