Current section

Files

Jump to
nex_core lib nex upload.ex
Raw

lib/nex/upload.ex

defmodule Nex.Upload do
@moduledoc """
File upload handling for Nex applications.
## Usage
Files are automatically parsed from multipart form data and available in `req.body`.
def post(req) do
# req.body["avatar"] is a %Plug.Upload{} struct
case req.body["avatar"] do
nil ->
Nex.json(%{error: "No file uploaded"}, status: 400)
upload ->
# Save to disk
case save(upload, "priv/uploads") do
{:ok, path} ->
Nex.json(%{url: path})
{:error, reason} ->
Nex.json(%{error: reason}, status: 500)
end
end
end
## File Validation
def post(req) do
upload = req.body["file"]
# Validate before saving
case validate(upload, max_size: 5_000_000, types: ["image/jpeg", "image/png"]) do
:ok ->
save(upload, "priv/uploads")
{:error, reason} ->
Nex.json(%{error: reason}, status: 400)
end
end
"""
@type upload :: %Plug.Upload{}
@type result :: {:ok, String.t()} | {:error, String.t()}
@doc """
Saves an uploaded file to disk.
## Arguments
* `upload` - %Plug.Upload{} struct from req.body
* `dir` - Directory to save to (created if not exists)
## Returns
* `{:ok, "/uploads/filename.jpg"}` - Relative URL path
* `{:error, "reason"}` - Error message
"""
def save(%Plug.Upload{path: source_path, filename: filename}, dir) do
# Sanitize filename to prevent path traversal
safe_filename = Path.basename(filename)
with :ok <- ensure_dir_exists(dir),
:ok <- copy_file(source_path, dir, safe_filename) do
{:ok, "/#{dir}/#{safe_filename}"}
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Saves with a custom filename (avoids filename conflicts).
## Arguments
* `upload` - %Plug.Upload{} struct
* `dir` - Directory to save to
* `options` - [prefix: "custom_"] to add prefix
"""
@spec save(upload(), String.t(), keyword()) :: result()
def save(%Plug.Upload{} = original_upload, dir, opts \\ []) do
prefix = Keyword.get(opts, :prefix, "")
ext = Path.extname(original_upload.filename)
base = Path.basename(original_upload.filename, ext)
unique_name = "#{prefix}#{base}_#{:os.system_time()}#{ext}"
upload = %Plug.Upload{
original_upload
| filename: unique_name
}
save(upload, dir)
end
@doc """
Validates an upload before saving.
## Options
* `:max_size` - Maximum file size in bytes (e.g., 5_000_000 for 5MB)
* `:types` - Allowed MIME types (e.g., ["image/jpeg", "image/png"])
* `:exts` - Allowed extensions (e.g., [".jpg", ".png"])
## Returns
* `:ok` - Validation passed
* `{:error, "reason"}` - Validation failed
"""
@spec validate(upload() | nil, keyword()) :: :ok | {:error, String.t()}
def validate(nil, _opts), do: {:error, "No file uploaded"}
def validate(%Plug.Upload{} = upload, opts) do
with :ok <- validate_size(upload, opts),
:ok <- validate_type(upload, opts),
:ok <- validate_ext(upload, opts) do
:ok
end
end
defp ensure_dir_exists(dir) do
case File.mkdir_p(dir) do
:ok -> :ok
{:error, reason} -> {:error, "Cannot create directory: #{inspect(reason)}"}
end
end
defp copy_file(source, dest_dir, filename) do
dest = Path.join(dest_dir, filename)
case File.cp(source, dest) do
:ok -> :ok
{:error, reason} -> {:error, "Cannot copy file: #{inspect(reason)}"}
end
end
defp validate_size(%Plug.Upload{path: path}, opts) do
case Keyword.get(opts, :max_size) do
nil ->
:ok
max_size ->
case File.stat(path) do
{:ok, %{size: size}} when size > max_size ->
{:error, "File too large. Maximum size is #{max_size} bytes"}
_ ->
:ok
end
end
end
defp validate_type(%Plug.Upload{content_type: type}, opts) do
case Keyword.get(opts, :types) do
nil ->
:ok
types ->
if type in types do
:ok
else
{:error, "Invalid file type. Allowed: #{Enum.join(types, ", ")}"}
end
end
end
defp validate_ext(%Plug.Upload{filename: filename}, opts) do
case Keyword.get(opts, :exts) do
nil ->
:ok
exts ->
ext = Path.extname(filename) |> String.downcase()
if ext in exts do
:ok
else
{:error, "Invalid file extension. Allowed: #{Enum.join(exts, ", ")}"}
end
end
end
end